From 9386ea43868b372e920e694de54273f10862b505 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Mon, 2 Apr 2018 23:28:10 +0200 Subject: [PATCH 1/3] watch ideas, first steps --- app.js | 3 ++ collections.js | 13 +++++ controllers/watches.js | 119 +++++++++++++++++++++++++++++++++++++++++ models/index.js | 5 +- models/watch/index.js | 70 ++++++++++++++++++++++++ models/watch/schema.js | 5 ++ routes/watches.js | 19 +++++++ serializers/index.js | 5 +- serializers/votes.js | 2 +- serializers/watches.js | 27 ++++++++++ test/watches.js | 102 +++++++++++++++++++++++++++++++++++ 11 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 controllers/watches.js create mode 100644 models/watch/index.js create mode 100644 models/watch/schema.js create mode 100644 routes/watches.js create mode 100644 serializers/watches.js create mode 100644 test/watches.js diff --git a/app.js b/app.js index 839067a..5b9180f 100644 --- a/app.js +++ b/app.js @@ -75,6 +75,9 @@ app.use('/ideas', require('./routes/ideas')); app.use('/ideas', require('./routes/votes')); app.use('/comments', require('./routes/votes')); +// watch ideas +app.use('/ideas', require('./routes/watches')); + // following are route factories // they need to know what is the primary object (i.e. idea, comment, etc.) app.use('/ideas', require('./routes/primary-comments')('idea')); diff --git a/collections.js b/collections.js index b460560..cca8162 100644 --- a/collections.js +++ b/collections.js @@ -130,6 +130,19 @@ module.exports = { unique: true } ] + }, + + watches: { + type: 'edge', + from: ['users'], + to: ['ideas'], + indexes: [ + { + type: 'hash', + fields: ['_from', '_to'], + unique: true + } + ] } }; diff --git a/controllers/watches.js b/controllers/watches.js new file mode 100644 index 0000000..adde1e1 --- /dev/null +++ b/controllers/watches.js @@ -0,0 +1,119 @@ +const path = require('path'), + models = require(path.resolve('./models')), + serializers = require(path.resolve('./serializers')); + +async function post(req, res, next) { + // read data from request + const { id } = req.params; + const { username } = req.auth; + + const primarys = req.baseUrl.substring(1); + + try { + const watch = await models.watch.create({ from: username, to: { type: primarys, id } }); + + const serializedWatch = serializers.serialize.watch(watch); + return res.status(201).json(serializedWatch); + } catch (e) { + return next(e); + } +} + +module.exports = { post }; +/* +const path = require('path'), + models = require(path.resolve('./models')), + serializers = require(path.resolve('./serializers')); + +/** + * Middleware to POST a vote to idea (and other objects in the future) + * / +async function post(req, res, next) { + console.log('&&&&&&&&&&&&&&&&&') + + // read data from request + const { id } = req.params; + const { username } = req.auth; + + console.log(id); + + // what is the type of the object we vote for (i.e. ideas, comments, ...) + const primarys = req.baseUrl.substring(1); + const primary = primarys.slice(0, -1); + + try { + console.log(primarys, '***'); + // save the vote to database + const vote = await models.vote.create({ from: username, to: { type: primarys, id }, value: 1 }); + // respond + const serializedVote = serializers.serialize.vote(vote); + return res.status(201).json(serializedVote); + } catch (e) { + // handle errors + switch (e.code) { + // duplicate vote + case 409: { + return res.status(409).json({ + errors: [{ + status: 409, + detail: 'duplicate vote' + }] + }); + } + // missing idea + case 404: { + console.log('.....'); + return res.status(404).json({ + errors: [{ + status: 404, + detail: `${primary} doesn't exist` + }] + }); + + } + default: { + return next(e); + } + } + } +} + +/** + * Middleware to DELETE a vote from an idea (and other objects in the future). + * / +async function del(req, res, next) { + + // read data from request + const { id } = req.params; + const { username } = req.auth; + + // what is the type of the object we vote for (i.e. ideas, comments, ...) + const primarys = req.baseUrl.substring(1); + const primary = primarys.slice(0, -1); + + try { + // remove the vote from database + await models.vote.remove({ from: username, to: { type: primarys, id } }); + // respond + return res.status(204).end(); + } catch (e) { + // handle errors + switch (e.code) { + // primary object or vote doesn't exist + case 404: { + return res.status(404).json({ + errors: [{ + status: 404, + detail: `vote or ${primary} doesn't exist` + }] + }); + } + default: { + return next(e); + } + } + } +} + +module.exports = { del, post }; +*/ diff --git a/models/index.js b/models/index.js index 58013cc..75613c1 100644 --- a/models/index.js +++ b/models/index.js @@ -9,7 +9,8 @@ const comment = require('./comment'), tag = require('./tag'), user = require('./user'), userTag = require('./user-tag'), - vote = require('./vote'); + vote = require('./vote'), + watch = require('./watch'); const models = { @@ -22,4 +23,4 @@ const models = { } }; -module.exports = Object.assign(models, { comment, contact, idea, ideaTag, message, model, tag, user, userTag, vote }); +module.exports = Object.assign(models, { comment, contact, idea, ideaTag, message, model, tag, user, userTag, vote, watch }); diff --git a/models/watch/index.js b/models/watch/index.js new file mode 100644 index 0000000..26cc054 --- /dev/null +++ b/models/watch/index.js @@ -0,0 +1,70 @@ +'use strict'; + +const path = require('path'); + +const Model = require(path.resolve('./models/model')), + schema = require('./schema'); + +class Watch extends Model { + + /** + * Create a watch. + * @param {string} from - username of the watch giver + * @param {object} to - receiver object of the watch + * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas') + * @param {string} to.id - id of the receiver + * @returns Promise - the saved watch + */ + static async create({ from: username, to: { type, id } }) { + // generate the watch + const watch = schema({ }); + + const query = ` + FOR u IN users FILTER u.username == @username + FOR i IN @@type FILTER i._key == @id + LET watch = MERGE(@watch, { _from: u._id, _to: i._id }) + INSERT watch INTO watches + + LET from = MERGE(KEEP(u, 'username'), u.profile) + LET to = MERGE(KEEP(i, 'title', 'detail', 'created'), { id: i._key }) + LET savedWatch = MERGE(KEEP(NEW, 'created'), { id: NEW._key }, { from }, { to }) + RETURN savedWatch`; + const params = { username, '@type': type, id, watch }; + const cursor = await this.db.query(query, params); + const out = await cursor.all(); + + // when nothing was created, throw error + if (out.length === 0) { + const e = new Error('not found'); + e.code = 404; + throw e; + } + + // what is the type of the object the watch was given to (important for serialization) + out[0].to.type = type; + + return out[0]; + } + + /** + * Read a watch from user to idea or something. + * @param {string} from - username of the watch giver + * @param {object} to - receiver object of the watch + * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas') + * @param {string} to.id - id of the receiver + * @returns Promise - the found watch or undefined + */ + static async read({ from: username, to: { type, id } }) { + const query = ` + FOR u IN users FILTER u.username == @username + FOR i IN @@type FILTER i._key == @id + FOR w IN watches FILTER w._from == u._id && w._to == i._id + RETURN w`; + const params = { username, '@type': type, id }; + const cursor = await this.db.query(query, params); + + return (await cursor.all())[0]; + } +} + +module.exports = Watch; diff --git a/models/watch/schema.js b/models/watch/schema.js new file mode 100644 index 0000000..d963502 --- /dev/null +++ b/models/watch/schema.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function ({ created = Date.now() }) { + return { created }; +}; diff --git a/routes/watches.js b/routes/watches.js new file mode 100644 index 0000000..028f8f0 --- /dev/null +++ b/routes/watches.js @@ -0,0 +1,19 @@ +'use strict'; + +const express = require('express'), + path = require('path'); + +const // authorize = require(path.resolve('./controllers/authorize')), + watchControllers = require(path.resolve('./controllers/watches')); + // voteValidators = require(path.resolve('./controllers/validators/votes')); + +// create router and controllers +const router = express.Router(); + +router.route('/:id/watches') + .post(/* authorize.onlyLogged, voteValidators.post,*/ watchControllers.post); + +// router.route('/:id/watches/watch') +// .delete(authorize.onlyLogged, voteValidators.del, voteControllers.del); + +module.exports = router; diff --git a/serializers/index.js b/serializers/index.js index dc8fdd8..ad83208 100644 --- a/serializers/index.js +++ b/serializers/index.js @@ -9,7 +9,8 @@ const comments = require('./comments'), messages = require('./messages'), tags = require('./tags'), users = require('./users'), - votes = require('./votes'); + votes = require('./votes'), + watches = require('./watches'); // deserializing const deserializer = new Deserializer({ @@ -44,6 +45,6 @@ function deserialize(req, res, next) { } module.exports = { - serialize: Object.assign({ }, comments, contacts, ideas, ideaTags, messages, tags, users, votes), + serialize: Object.assign({ }, comments, contacts, ideas, ideaTags, messages, tags, users, votes, watches), deserialize }; diff --git a/serializers/votes.js b/serializers/votes.js index 776350a..139dcf8 100644 --- a/serializers/votes.js +++ b/serializers/votes.js @@ -4,7 +4,7 @@ const Serializer = require('jsonapi-serializer').Serializer; const voteSerializer = new Serializer('votes', { id: 'id', - attributes: ['title', 'detail', 'created', 'value', 'from', 'to'], + attributes: ['created', 'value', 'from', 'to'], keyForAttribute: 'camelCase', typeForAttribute(attribute, doc) { if (attribute === 'from') return 'users'; diff --git a/serializers/watches.js b/serializers/watches.js new file mode 100644 index 0000000..cdc016b --- /dev/null +++ b/serializers/watches.js @@ -0,0 +1,27 @@ +'use strict'; + +const Serializer = require('jsonapi-serializer').Serializer; + +const voteSerializer = new Serializer('watches', { + id: 'id', + attributes: ['created', 'from', 'to'], + keyForAttribute: 'camelCase', + typeForAttribute(attribute, doc) { + if (attribute === 'from') return 'users'; + if (attribute === 'to') return doc.type; + }, + from: { + ref: 'username', + type: 'users' + }, + to: { + ref: 'id', + type: 'ideas' + } +}); + +function watch(data) { + return voteSerializer.serialize(data); +} + +module.exports = { watch }; diff --git a/test/watches.js b/test/watches.js new file mode 100644 index 0000000..603ce48 --- /dev/null +++ b/test/watches.js @@ -0,0 +1,102 @@ +'use strict'; + +const path = require('path'), + should = require('should'); + +const agentFactory = require('./agent'), + dbHandle = require('./handle-database'), + models = require(path.resolve('./models')); + +describe('Watch ideas.', () => { + let agent, + dbData, + loggedUser; + + afterEach(async () => { + await dbHandle.clear(); + }); + + beforeEach(() => { + agent = agentFactory(); + }); + + describe('Start watching idea `POST /ideas/:id/watches`', () => { + + let idea0; + + beforeEach(async () => { + const data = { + users: 1, // how many users to make + verifiedUsers: [0], // which users to make verified + ideas: [[{}, 0]] + }; + // create data in database + dbData = await dbHandle.fill(data); + + loggedUser = dbData.users[0]; + idea0 = dbData.ideas[0]; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid request', () => { + + it('Success 201', async () => { + const response = await agent.post(`/ideas/${idea0.id}/watches`) + .send({ + data: { + type: 'watches' + } + }) + .expect(201); + + // saved into database? + const dbWatch = await models.watch.read({ from: loggedUser.username, to: { type: 'ideas', id: idea0.id } }); + should(dbWatch).ok(); + + // correct response? + should(response.body).match({ + data: { + type: 'watches', + relationships: { + from: { data: { type: 'users', id: loggedUser.username } }, + to: { data: { type: 'ideas', id: idea0.id } } + } + } + }); + }); + + it('Duplicate 409'); + it('Nonexistent idea 404'); + }); + + context('invalid request', () => { + it('invalid idea id'); + }); + }); + }); + + describe('Stop watching idea `DELETE /ideas/:id/watches/watch`', () => { + it('todo'); + }); + + describe('See who watches the idea `GET /ideas/:id/watches`', () => { + it('todo'); + }); + + describe('Include info whether I watch the idea when reading it', () => { + it('todo'); + }); + + describe('See ideas watched by given user(s) `GET /ideas/:id?filter[watchedBy]=username1,username2`', () => { + it('todo'); + }); + + describe('How many users watch the idea?', () => { + it('todo'); + }); +}); From 12312c1cb961dd0de1f26a74824bcae814daead1 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Tue, 3 Apr 2018 20:27:17 +0200 Subject: [PATCH 2/3] POST /ideas/:id/cares done --- app.js | 4 +- collections.js | 2 +- controllers/{watches.js => cares.js} | 32 ++++- controllers/validators/cares.js | 8 ++ controllers/validators/index.js | 1 + controllers/validators/schema/cares.js | 31 +++++ controllers/validators/schema/index.js | 3 +- models/{watch => care}/index.js | 30 ++--- models/{watch => care}/schema.js | 0 models/index.js | 8 +- routes/cares.js | 19 +++ routes/watches.js | 19 --- serializers/{watches.js => cares.js} | 6 +- serializers/index.js | 8 +- test/cares.js | 159 +++++++++++++++++++++++++ test/watches.js | 102 ---------------- 16 files changed, 277 insertions(+), 155 deletions(-) rename controllers/{watches.js => cares.js} (78%) create mode 100644 controllers/validators/cares.js create mode 100644 controllers/validators/schema/cares.js rename models/{watch => care}/index.js (67%) rename models/{watch => care}/schema.js (100%) create mode 100644 routes/cares.js delete mode 100644 routes/watches.js rename serializers/{watches.js => cares.js} (81%) create mode 100644 test/cares.js delete mode 100644 test/watches.js diff --git a/app.js b/app.js index 5b9180f..34e0bcd 100644 --- a/app.js +++ b/app.js @@ -75,8 +75,8 @@ app.use('/ideas', require('./routes/ideas')); app.use('/ideas', require('./routes/votes')); app.use('/comments', require('./routes/votes')); -// watch ideas -app.use('/ideas', require('./routes/watches')); +// care about ideas +app.use('/ideas', require('./routes/cares')); // following are route factories // they need to know what is the primary object (i.e. idea, comment, etc.) diff --git a/collections.js b/collections.js index cca8162..3fc2ccf 100644 --- a/collections.js +++ b/collections.js @@ -132,7 +132,7 @@ module.exports = { ] }, - watches: { + cares: { type: 'edge', from: ['users'], to: ['ideas'], diff --git a/controllers/watches.js b/controllers/cares.js similarity index 78% rename from controllers/watches.js rename to controllers/cares.js index adde1e1..dfccc30 100644 --- a/controllers/watches.js +++ b/controllers/cares.js @@ -8,14 +8,38 @@ async function post(req, res, next) { const { username } = req.auth; const primarys = req.baseUrl.substring(1); + const primary = primarys.slice(0, -1); try { - const watch = await models.watch.create({ from: username, to: { type: primarys, id } }); + const care = await models.care.create({ from: username, to: { type: primarys, id } }); - const serializedWatch = serializers.serialize.watch(watch); - return res.status(201).json(serializedWatch); + const serializedCare = serializers.serialize.care(care); + return res.status(201).json(serializedCare); } catch (e) { - return next(e); + // handle errors + switch (e.code) { + // duplicate vote + case 409: { + return res.status(409).json({ + errors: [{ + status: 409, + detail: 'duplicate care' + }] + }); + } + // missing idea + case 404: { + return res.status(404).json({ + errors: [{ + status: 404, + detail: `${primary} doesn't exist` + }] + }); + } + default: { + return next(e); + } + } } } diff --git a/controllers/validators/cares.js b/controllers/validators/cares.js new file mode 100644 index 0000000..9c1749d --- /dev/null +++ b/controllers/validators/cares.js @@ -0,0 +1,8 @@ +'use strict'; + +const validate = require('./validate-by-schema'); + +const del = validate('deleteCare'); +const post = validate('postCares'); + +module.exports = { del, post }; diff --git a/controllers/validators/index.js b/controllers/validators/index.js index 1aa179e..eb05a12 100644 --- a/controllers/validators/index.js +++ b/controllers/validators/index.js @@ -2,6 +2,7 @@ exports.authenticate = require('./authenticate'); exports.avatar = require('./avatar'); +exports.cares = require('./cares'); exports.comments = require('./comments'); exports.contacts = require('./contacts'); exports.messages = require('./messages'); diff --git a/controllers/validators/schema/cares.js b/controllers/validators/schema/cares.js new file mode 100644 index 0000000..7fa3e5d --- /dev/null +++ b/controllers/validators/schema/cares.js @@ -0,0 +1,31 @@ +'use strict'; + +const { id } = require('./paths'); + +const postCares = { + properties: { + params: { + properties: { id }, + required: ['id'], + additionalProperties: false + }, + body: { + additionalProperties: false + }, + required: ['body', 'params'] + } +}; +/* +const deleteCare = { + properties: { + params: { + properties: { id }, + required: ['id'], + additionalProperties: false + }, + required: ['params'] + } +}; +*/ + +module.exports = { /* deleteCare, */postCares }; diff --git a/controllers/validators/schema/index.js b/controllers/validators/schema/index.js index a681495..c80bc4a 100644 --- a/controllers/validators/schema/index.js +++ b/controllers/validators/schema/index.js @@ -3,6 +3,7 @@ const account = require('./account'), authenticate = require('./authenticate'), avatar = require('./avatar'), + cares = require('./cares'), comments = require('./comments'), contacts = require('./contacts'), definitions = require('./definitions'), @@ -16,5 +17,5 @@ const account = require('./account'), votes = require('./votes'); -module.exports = Object.assign({ definitions }, account, authenticate, avatar, +module.exports = Object.assign({ definitions }, account, authenticate, avatar, cares, comments, contacts, ideas, ideaTags, messages, params, tags, users, userTags, votes); diff --git a/models/watch/index.js b/models/care/index.js similarity index 67% rename from models/watch/index.js rename to models/care/index.js index 26cc054..4031ff0 100644 --- a/models/watch/index.js +++ b/models/care/index.js @@ -8,28 +8,28 @@ const Model = require(path.resolve('./models/model')), class Watch extends Model { /** - * Create a watch. - * @param {string} from - username of the watch giver - * @param {object} to - receiver object of the watch + * Create a care. + * @param {string} from - username of the care giver + * @param {object} to - receiver object of the care * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas') * @param {string} to.id - id of the receiver - * @returns Promise - the saved watch + * @returns Promise - the saved care */ static async create({ from: username, to: { type, id } }) { - // generate the watch - const watch = schema({ }); + // generate the care + const care = schema({ }); const query = ` FOR u IN users FILTER u.username == @username FOR i IN @@type FILTER i._key == @id - LET watch = MERGE(@watch, { _from: u._id, _to: i._id }) - INSERT watch INTO watches + LET care = MERGE(@care, { _from: u._id, _to: i._id }) + INSERT care INTO cares LET from = MERGE(KEEP(u, 'username'), u.profile) LET to = MERGE(KEEP(i, 'title', 'detail', 'created'), { id: i._key }) LET savedWatch = MERGE(KEEP(NEW, 'created'), { id: NEW._key }, { from }, { to }) RETURN savedWatch`; - const params = { username, '@type': type, id, watch }; + const params = { username, '@type': type, id, care }; const cursor = await this.db.query(query, params); const out = await cursor.all(); @@ -40,25 +40,25 @@ class Watch extends Model { throw e; } - // what is the type of the object the watch was given to (important for serialization) + // what is the type of the object the care was given to (important for serialization) out[0].to.type = type; return out[0]; } /** - * Read a watch from user to idea or something. - * @param {string} from - username of the watch giver - * @param {object} to - receiver object of the watch + * Read a care from user to idea or something. + * @param {string} from - username of the care giver + * @param {object} to - receiver object of the care * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas') * @param {string} to.id - id of the receiver - * @returns Promise - the found watch or undefined + * @returns Promise - the found care or undefined */ static async read({ from: username, to: { type, id } }) { const query = ` FOR u IN users FILTER u.username == @username FOR i IN @@type FILTER i._key == @id - FOR w IN watches FILTER w._from == u._id && w._to == i._id + FOR w IN cares FILTER w._from == u._id && w._to == i._id RETURN w`; const params = { username, '@type': type, id }; const cursor = await this.db.query(query, params); diff --git a/models/watch/schema.js b/models/care/schema.js similarity index 100% rename from models/watch/schema.js rename to models/care/schema.js diff --git a/models/index.js b/models/index.js index 75613c1..97e269a 100644 --- a/models/index.js +++ b/models/index.js @@ -1,6 +1,7 @@ 'use strict'; -const comment = require('./comment'), +const care = require('./care'), + comment = require('./comment'), contact = require('./contact'), idea = require('./idea'), ideaTag = require('./idea-tag'), @@ -9,8 +10,7 @@ const comment = require('./comment'), tag = require('./tag'), user = require('./user'), userTag = require('./user-tag'), - vote = require('./vote'), - watch = require('./watch'); + vote = require('./vote'); const models = { @@ -23,4 +23,4 @@ const models = { } }; -module.exports = Object.assign(models, { comment, contact, idea, ideaTag, message, model, tag, user, userTag, vote, watch }); +module.exports = Object.assign(models, { care, comment, contact, idea, ideaTag, message, model, tag, user, userTag, vote }); diff --git a/routes/cares.js b/routes/cares.js new file mode 100644 index 0000000..88450e7 --- /dev/null +++ b/routes/cares.js @@ -0,0 +1,19 @@ +'use strict'; + +const express = require('express'), + path = require('path'); + +const authorize = require(path.resolve('./controllers/authorize')), + careControllers = require(path.resolve('./controllers/cares')), + careValidators = require(path.resolve('./controllers/validators/cares')); + +// create router and controllers +const router = express.Router(); + +router.route('/:id/cares') + .post(authorize.onlyLogged, careValidators.post, careControllers.post); + +// router.route('/:id/cares/care') +// .delete(authorize.onlyLogged, voteValidators.del, voteControllers.del); + +module.exports = router; diff --git a/routes/watches.js b/routes/watches.js deleted file mode 100644 index 028f8f0..0000000 --- a/routes/watches.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const express = require('express'), - path = require('path'); - -const // authorize = require(path.resolve('./controllers/authorize')), - watchControllers = require(path.resolve('./controllers/watches')); - // voteValidators = require(path.resolve('./controllers/validators/votes')); - -// create router and controllers -const router = express.Router(); - -router.route('/:id/watches') - .post(/* authorize.onlyLogged, voteValidators.post,*/ watchControllers.post); - -// router.route('/:id/watches/watch') -// .delete(authorize.onlyLogged, voteValidators.del, voteControllers.del); - -module.exports = router; diff --git a/serializers/watches.js b/serializers/cares.js similarity index 81% rename from serializers/watches.js rename to serializers/cares.js index cdc016b..8892fd2 100644 --- a/serializers/watches.js +++ b/serializers/cares.js @@ -2,7 +2,7 @@ const Serializer = require('jsonapi-serializer').Serializer; -const voteSerializer = new Serializer('watches', { +const voteSerializer = new Serializer('cares', { id: 'id', attributes: ['created', 'from', 'to'], keyForAttribute: 'camelCase', @@ -20,8 +20,8 @@ const voteSerializer = new Serializer('watches', { } }); -function watch(data) { +function care(data) { return voteSerializer.serialize(data); } -module.exports = { watch }; +module.exports = { care }; diff --git a/serializers/index.js b/serializers/index.js index ad83208..3bc24ef 100644 --- a/serializers/index.js +++ b/serializers/index.js @@ -2,15 +2,15 @@ const Deserializer = require('jsonapi-serializer').Deserializer; -const comments = require('./comments'), +const cares = require('./cares'), + comments = require('./comments'), contacts = require('./contacts'), ideas = require('./ideas'), ideaTags = require('./idea-tags'), messages = require('./messages'), tags = require('./tags'), users = require('./users'), - votes = require('./votes'), - watches = require('./watches'); + votes = require('./votes'); // deserializing const deserializer = new Deserializer({ @@ -45,6 +45,6 @@ function deserialize(req, res, next) { } module.exports = { - serialize: Object.assign({ }, comments, contacts, ideas, ideaTags, messages, tags, users, votes, watches), + serialize: Object.assign({ }, cares, comments, contacts, ideas, ideaTags, messages, tags, users, votes), deserialize }; diff --git a/test/cares.js b/test/cares.js new file mode 100644 index 0000000..082cae7 --- /dev/null +++ b/test/cares.js @@ -0,0 +1,159 @@ +'use strict'; + +const path = require('path'), + should = require('should'); + +const agentFactory = require('./agent'), + dbHandle = require('./handle-database'), + models = require(path.resolve('./models')); + +describe('Expressing interest in ideas (caring about ideas).', () => { + let agent, + dbData, + loggedUser; + + afterEach(async () => { + await dbHandle.clear(); + }); + + beforeEach(() => { + agent = agentFactory(); + }); + + describe('Start caring about idea `POST /ideas/:id/cares`', () => { + + let idea0; + let requestBody; + + beforeEach(async () => { + const data = { + users: 1, // how many users to make + verifiedUsers: [0], // which users to make verified + ideas: [[{}, 0]] + }; + // create data in database + dbData = await dbHandle.fill(data); + + loggedUser = dbData.users[0]; + idea0 = dbData.ideas[0]; + }); + + beforeEach(() => { + requestBody = { + data: { + type: 'cares' + } + }; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid request', () => { + + it('Success 201', async () => { + const response = await agent.post(`/ideas/${idea0.id}/cares`) + .send(requestBody) + .expect(201); + + // saved into database? + const dbCare = await models.care.read({ from: loggedUser.username, to: { type: 'ideas', id: idea0.id } }); + should(dbCare).ok(); + + // correct response? + should(response.body).match({ + data: { + type: 'cares', + relationships: { + from: { data: { type: 'users', id: loggedUser.username } }, + to: { data: { type: 'ideas', id: idea0.id } } + } + } + }); + }); + + it('Duplicate 409', async () => { + await agent.post(`/ideas/${idea0.id}/cares`) + .send(requestBody) + .expect(201); + + await agent.post(`/ideas/${idea0.id}/cares`) + .send(requestBody) + .expect(409); + }); + + it('Nonexistent idea 404', async () => { + await agent.post('/ideas/11111111/cares') + .send(requestBody) + .expect(404); + }); + }); + + context('invalid request', () => { + it('[invalid idea id] 400', async () => { + await agent.post('/ideas/invalid--id/cares') + .send(requestBody) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent.post(`/ideas/${idea0.id}/cares`) + .send(requestBody) + .expect(403); + }); + }); + }); + + describe('Stop caring about idea `DELETE /ideas/:id/cares/care`', () => { + context('logged', () => { + context('valid', () => { + it('[care exists] 204 and remove from database', async () => { + + // first care should exist + const dbCareBefore = await models.care.read({ from: loggedUser.username, to: { type: 'ideas', id: idea0.id } }); + should(dbCareBefore).ok(); + + await agent.delete(`/ideas/${idea0.id}/cares/care`) + .expect(204); + + // then care shouldn't exist + const dbCareAfter = await models.care.read({ from: loggedUser.username, to: { type: 'ideas', id: idea0.id } }); + should(dbCareAfter).not.ok(); + }); + + it('[care doesn\'t exist] 404'); + it('[idea doesn\'t exist] 404'); + }); + + context('invalid', () => { + it('[invalid idea id] 400'); + }); + }); + + context('not logged', () => { + it('403'); + }); + }); + + describe('See who cares about the idea `GET /ideas/:id/cares`', () => { + it('todo'); + }); + + describe('Include info whether I care about the idea when reading it', () => { + it('todo'); + }); + + describe('See ideas cared for by given user(s) `GET /ideas/:id?filter[caring]=username1,username2`', () => { + it('todo'); + }); + + describe('How many users care about the idea?', () => { + it('todo'); + }); +}); diff --git a/test/watches.js b/test/watches.js deleted file mode 100644 index 603ce48..0000000 --- a/test/watches.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -const path = require('path'), - should = require('should'); - -const agentFactory = require('./agent'), - dbHandle = require('./handle-database'), - models = require(path.resolve('./models')); - -describe('Watch ideas.', () => { - let agent, - dbData, - loggedUser; - - afterEach(async () => { - await dbHandle.clear(); - }); - - beforeEach(() => { - agent = agentFactory(); - }); - - describe('Start watching idea `POST /ideas/:id/watches`', () => { - - let idea0; - - beforeEach(async () => { - const data = { - users: 1, // how many users to make - verifiedUsers: [0], // which users to make verified - ideas: [[{}, 0]] - }; - // create data in database - dbData = await dbHandle.fill(data); - - loggedUser = dbData.users[0]; - idea0 = dbData.ideas[0]; - }); - - context('logged in', () => { - - beforeEach(() => { - agent = agentFactory.logged(loggedUser); - }); - - context('valid request', () => { - - it('Success 201', async () => { - const response = await agent.post(`/ideas/${idea0.id}/watches`) - .send({ - data: { - type: 'watches' - } - }) - .expect(201); - - // saved into database? - const dbWatch = await models.watch.read({ from: loggedUser.username, to: { type: 'ideas', id: idea0.id } }); - should(dbWatch).ok(); - - // correct response? - should(response.body).match({ - data: { - type: 'watches', - relationships: { - from: { data: { type: 'users', id: loggedUser.username } }, - to: { data: { type: 'ideas', id: idea0.id } } - } - } - }); - }); - - it('Duplicate 409'); - it('Nonexistent idea 404'); - }); - - context('invalid request', () => { - it('invalid idea id'); - }); - }); - }); - - describe('Stop watching idea `DELETE /ideas/:id/watches/watch`', () => { - it('todo'); - }); - - describe('See who watches the idea `GET /ideas/:id/watches`', () => { - it('todo'); - }); - - describe('Include info whether I watch the idea when reading it', () => { - it('todo'); - }); - - describe('See ideas watched by given user(s) `GET /ideas/:id?filter[watchedBy]=username1,username2`', () => { - it('todo'); - }); - - describe('How many users watch the idea?', () => { - it('todo'); - }); -}); From 6cb2104181b470e24b13d59bd08fcfb1ab0f8f89 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Tue, 3 Apr 2018 20:58:17 +0200 Subject: [PATCH 3/3] DELETE /ideas/:id/cares/care --- controllers/cares.js | 79 ++++---------------------- controllers/validators/schema/cares.js | 5 +- models/care/index.js | 24 ++++++++ routes/cares.js | 5 +- test/cares.js | 57 ++++++++++++++++--- test/handle-database.js | 23 +++++++- 6 files changed, 111 insertions(+), 82 deletions(-) diff --git a/controllers/cares.js b/controllers/cares.js index dfccc30..6e2aa8c 100644 --- a/controllers/cares.js +++ b/controllers/cares.js @@ -2,6 +2,9 @@ const path = require('path'), models = require(path.resolve('./models')), serializers = require(path.resolve('./serializers')); +/** + * Middleware to add a care to idea. + */ async function post(req, res, next) { // read data from request const { id } = req.params; @@ -18,7 +21,7 @@ async function post(req, res, next) { } catch (e) { // handle errors switch (e.code) { - // duplicate vote + // duplicate care case 409: { return res.status(409).json({ errors: [{ @@ -43,92 +46,33 @@ async function post(req, res, next) { } } -module.exports = { post }; -/* -const path = require('path'), - models = require(path.resolve('./models')), - serializers = require(path.resolve('./serializers')); - -/** - * Middleware to POST a vote to idea (and other objects in the future) - * / -async function post(req, res, next) { - console.log('&&&&&&&&&&&&&&&&&') - - // read data from request - const { id } = req.params; - const { username } = req.auth; - - console.log(id); - - // what is the type of the object we vote for (i.e. ideas, comments, ...) - const primarys = req.baseUrl.substring(1); - const primary = primarys.slice(0, -1); - - try { - console.log(primarys, '***'); - // save the vote to database - const vote = await models.vote.create({ from: username, to: { type: primarys, id }, value: 1 }); - // respond - const serializedVote = serializers.serialize.vote(vote); - return res.status(201).json(serializedVote); - } catch (e) { - // handle errors - switch (e.code) { - // duplicate vote - case 409: { - return res.status(409).json({ - errors: [{ - status: 409, - detail: 'duplicate vote' - }] - }); - } - // missing idea - case 404: { - console.log('.....'); - return res.status(404).json({ - errors: [{ - status: 404, - detail: `${primary} doesn't exist` - }] - }); - - } - default: { - return next(e); - } - } - } -} - /** - * Middleware to DELETE a vote from an idea (and other objects in the future). - * / + * Middleware to DELETE a care from an idea (and other objects in the future). + */ async function del(req, res, next) { // read data from request const { id } = req.params; const { username } = req.auth; - // what is the type of the object we vote for (i.e. ideas, comments, ...) + // what is the type of the object we care for (i.e. ideas, comments, ...) const primarys = req.baseUrl.substring(1); const primary = primarys.slice(0, -1); try { - // remove the vote from database - await models.vote.remove({ from: username, to: { type: primarys, id } }); + // remove the care from database + await models.care.remove({ from: username, to: { type: primarys, id } }); // respond return res.status(204).end(); } catch (e) { // handle errors switch (e.code) { - // primary object or vote doesn't exist + // primary object or care doesn't exist case 404: { return res.status(404).json({ errors: [{ status: 404, - detail: `vote or ${primary} doesn't exist` + detail: `care or ${primary} doesn't exist` }] }); } @@ -140,4 +84,3 @@ async function del(req, res, next) { } module.exports = { del, post }; -*/ diff --git a/controllers/validators/schema/cares.js b/controllers/validators/schema/cares.js index 7fa3e5d..f44df28 100644 --- a/controllers/validators/schema/cares.js +++ b/controllers/validators/schema/cares.js @@ -15,7 +15,7 @@ const postCares = { required: ['body', 'params'] } }; -/* + const deleteCare = { properties: { params: { @@ -26,6 +26,5 @@ const deleteCare = { required: ['params'] } }; -*/ -module.exports = { /* deleteCare, */postCares }; +module.exports = { deleteCare, postCares }; diff --git a/models/care/index.js b/models/care/index.js index 4031ff0..1fd297e 100644 --- a/models/care/index.js +++ b/models/care/index.js @@ -65,6 +65,30 @@ class Watch extends Model { return (await cursor.all())[0]; } + + /** + * Remove a care. + * @param {string} from - username of the care giver + * @param {object} to - receiver object of the care + * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas') + * @param {string} to.id - id of the receiver + * @returns Promise + */ + static async remove({ from: username, to: { type, id } }) { + const query = ` + FOR u IN users FILTER u.username == @username + FOR i IN @@type FILTER i._key == @id + FOR v IN cares FILTER v._from == u._id AND v._to == i._id + REMOVE v IN cares`; + const params = { username, '@type': type, id }; + const cursor = await this.db.query(query, params); + + if (cursor.extra.stats.writesExecuted === 0) { + const e = new Error('primary or care not found'); + e.code = 404; + throw e; + } + } } module.exports = Watch; diff --git a/routes/cares.js b/routes/cares.js index 88450e7..d0951b1 100644 --- a/routes/cares.js +++ b/routes/cares.js @@ -7,13 +7,12 @@ const authorize = require(path.resolve('./controllers/authorize')), careControllers = require(path.resolve('./controllers/cares')), careValidators = require(path.resolve('./controllers/validators/cares')); -// create router and controllers const router = express.Router(); router.route('/:id/cares') .post(authorize.onlyLogged, careValidators.post, careControllers.post); -// router.route('/:id/cares/care') -// .delete(authorize.onlyLogged, voteValidators.del, voteControllers.del); +router.route('/:id/cares/care') + .delete(authorize.onlyLogged, careValidators.del, careControllers.del); module.exports = router; diff --git a/test/cares.js b/test/cares.js index 082cae7..f8302e1 100644 --- a/test/cares.js +++ b/test/cares.js @@ -22,8 +22,8 @@ describe('Expressing interest in ideas (caring about ideas).', () => { describe('Start caring about idea `POST /ideas/:id/cares`', () => { - let idea0; - let requestBody; + let idea0, + requestBody; beforeEach(async () => { const data = { @@ -111,8 +111,35 @@ describe('Expressing interest in ideas (caring about ideas).', () => { }); describe('Stop caring about idea `DELETE /ideas/:id/cares/care`', () => { + + let idea0; + + beforeEach(async () => { + const data = { + users: 2, // how many users to make + verifiedUsers: [0, 1], // which users to make verified + ideas: [[{}, 0], [{}, 0]], + cares: [ + [0, ['ideas', 0]], + [0, ['ideas', 1]], + [1, ['ideas', 0]] + ] + }; + // create data in database + dbData = await dbHandle.fill(data); + + loggedUser = dbData.users[0]; + idea0 = dbData.ideas[0]; + }); + context('logged', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + context('valid', () => { + it('[care exists] 204 and remove from database', async () => { // first care should exist @@ -127,17 +154,33 @@ describe('Expressing interest in ideas (caring about ideas).', () => { should(dbCareAfter).not.ok(); }); - it('[care doesn\'t exist] 404'); - it('[idea doesn\'t exist] 404'); + it('[care doesn\'t exist] 404', async () => { + await agent.delete(`/ideas/${idea0.id}/cares/care`) + .expect(204); + + await agent.delete(`/ideas/${idea0.id}/cares/care`) + .expect(404); + }); + + it('[idea doesn\'t exist] 404', async () => { + await agent.delete('/ideas/0000000/cares/care') + .expect(404); + }); }); - + context('invalid', () => { - it('[invalid idea id] 400'); + it('[invalid idea id] 400', async () => { + await agent.delete('/ideas/invalid/cares/care') + .expect(400); + }); }); }); context('not logged', () => { - it('403'); + it('403', async () => { + await agent.delete(`/ideas/${idea0.id}/cares/care`) + .expect(403); + }); }); }); diff --git a/test/handle-database.js b/test/handle-database.js index b0fb873..c99a378 100644 --- a/test/handle-database.js +++ b/test/handle-database.js @@ -25,7 +25,8 @@ exports.fill = async function (data) { ideaTags: [], ideaComments: [], reactions: [], - votes: [] + votes: [], + cares: [] }; data = _.defaults(data, def); @@ -138,6 +139,16 @@ exports.fill = async function (data) { vote.id = newVote.id; } + for (const care of processed.cares) { + const from = care.from.username; + const to = { type: care._to.type, id: care.to.id }; + + const newCare = await models.care.create({ from, to }); + + // save the care's id + care.id = newCare.id; + } + return processed; }; @@ -338,5 +349,15 @@ function processData(data) { }; }); + output.cares = data.cares.map(([from, to]) => { + const [type, id] = to; + return { + _from: from, + _to: { type, id }, + get from() { return output.users[this._from]; }, + get to() { return output[this._to.type][this._to.id]; } + }; + }); + return output; }