From 3002db3d75690a727a5752e86da934dbde24c779 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 29 Nov 2015 19:05:24 +0100 Subject: [PATCH] add ability to update tasks, can pass additional fields to Model.sanitize at runtime --- common/locales/en/api-v3.json | 3 +- test/api/v3/unit/libs/baseModel.test.js | 14 +++++++++ website/src/controllers/api-v3/tasks.js | 41 +++++++++++++++++++++++-- website/src/libs/api-v3/baseModel.js | 5 +-- website/src/models/task.js | 8 +++-- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 5227ffde67..fd28563c87 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -17,5 +17,6 @@ "invalidReqParams": "Invalid request parameters.", "taskIdRequired": "\"taskId\" must be a valid UUID", "taskNotFound": "Task not found.", - "invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\"." + "invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".", + "cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted." } diff --git a/test/api/v3/unit/libs/baseModel.test.js b/test/api/v3/unit/libs/baseModel.test.js index 3b50a7f4f5..98835b1c29 100644 --- a/test/api/v3/unit/libs/baseModel.test.js +++ b/test/api/v3/unit/libs/baseModel.test.js @@ -43,6 +43,20 @@ describe('Base model plugin', () => { expect(sanitized.noUpdateForMe).to.equal(undefined); }); + it('accepts an array of additional fields to sanitize at runtime', () => { + baseModel(schema, { + noSet: ['noUpdateForMe'] + }); + + expect(schema.statics.sanitize).to.exist; + let sanitized = schema.statics.sanitize({ok: true, noUpdateForMe: true, usuallySettable: true}, ['usuallySettable']); + + expect(sanitized).to.have.property('ok'); + expect(sanitized).not.to.have.property('noUpdateForMe'); + expect(sanitized).not.to.have.property('usuallySettable'); + }); + + it('can make fields private', () => { baseModel(schema, { private: ['amPrivate'] diff --git a/website/src/controllers/api-v3/tasks.js b/website/src/controllers/api-v3/tasks.js index 1b9eddeb7c..e6f9eb41e4 100644 --- a/website/src/controllers/api-v3/tasks.js +++ b/website/src/controllers/api-v3/tasks.js @@ -5,6 +5,7 @@ import { NotAuthorized, } from '../../libs/api-v3/errors'; import Q from 'q'; +import _ from 'lodash'; let api = {}; @@ -26,7 +27,7 @@ api.createTask = { let user = res.locals.user; let taskType = req.body.type; - let newTask = new Tasks[`${taskType.charAt(0).toUpperCase() + taskType.slice(1)}Model`](Tasks.Task.sanitize(req.body)); + let newTask = new Tasks[`${taskType.charAt(0).toUpperCase() + taskType.slice(1)}Model`](Tasks.TaskModel.sanitize(req.body)); newTask.userId = user._id; user.tasksOrder[taskType].unshift(newTask._id); @@ -35,8 +36,8 @@ api.createTask = { newTask.save(), user.save(), ]) - .then(([task]) => res.respond(201, task)) - .catch(next); + .then(([task]) => res.respond(201, task)) + .catch(next); }, }; @@ -99,6 +100,40 @@ api.getTask = { }, }; +/** + * @api {put} /task/:taskId Update a task + * @apiVersion 3.0.0 + * @apiName GetTask + * @apiGroup Task + * + * @apiParam {UUID} taskId The task _id + * + * @apiSuccess {object} task The updated task + */ +api.updateTask = { + method: 'GET', + url: '/tasks/:taskId', + middlewares: [authWithHeaders()], + handler (req, res, next) { + let user = res.locals.user; + req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); + + Tasks.TaskModel.findOne({ + _id: req.params.taskId, + userId: user._id, + }).exec() + .then((task) => { + if (!task) throw new NotFound(res.t('taskNotFound')); + // TODO merge goes deep into objects, it's ok? + // TODO also check that array fields are updated correctly without marking modified + _.merge(task, Tasks.TaskModel.sanitizeUpdate(req.body)); + return task.save(); + }) + .then((savedTask) => res.respond(200, savedTask)) + .catch(next); + }, +}; + // Remove a task from user.tasksOrder function _removeTaskTasksOrder (user, taskId) { let types = ['habits', 'dailys', 'todos', 'rewards']; diff --git a/website/src/libs/api-v3/baseModel.js b/website/src/libs/api-v3/baseModel.js index 2de770226b..c0331bf924 100644 --- a/website/src/libs/api-v3/baseModel.js +++ b/website/src/libs/api-v3/baseModel.js @@ -35,8 +35,9 @@ export default function baseModel (schema, options = {}) { let privateFields = ['__v']; if (Array.isArray(options.noSet)) noSetFields.push(...options.noSet); - schema.statics.sanitize = function sanitize (objToSanitize = {}) { - noSetFields.forEach((fieldPath) => { + // This method accepts an additional array of fields to be sanitized that can be passed at runtime + schema.statics.sanitize = function sanitize (objToSanitize = {}, additionalFields = []) { + noSetFields.concat(additionalFields).forEach((fieldPath) => { objectPath.del(objToSanitize, fieldPath); }); diff --git a/website/src/models/task.js b/website/src/models/task.js index e7760642bf..0fecf5f0ed 100644 --- a/website/src/models/task.js +++ b/website/src/models/task.js @@ -42,9 +42,13 @@ TaskSchema.plugin(baseModel, { timestamps: true, }); -export let TaskModel = mongoose.model('Task', TaskSchema); +// A list of additional fields that cannot be updated (but can be set on creation) +let noUpdate = ['_id', 'type']; +TaskSchema.statics.sanitizeUpdate = function sanitizeUpdate (updateObj) { + return TaskModel.sanitize(updateObj, noUpdate); // eslint-disable-line no-use-before-define +}; -// TODO discriminators: it's very important to check that the options and plugins of the parent schema are used in the sub-schemas too +export let TaskModel = mongoose.model('Task', TaskSchema); // habits and dailies shared fields let habitDailySchema = () => {