diff --git a/src/app/misc.coffee b/src/app/misc.coffee index ee3ad2c173..ef27421631 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -5,7 +5,7 @@ helpers = require('habitrpg-shared/script/helpers') module.exports.batchTxn = batchTxn = (model, cb, options) -> user = options?.user or model.at("_user") - uObj = hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116 + uObj = helpers.hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116 batch = set: (k,v) -> helpers.dotSet(k,v,uObj); paths[k] = true get: (k) -> helpers.dotGet(k,uObj) @@ -87,18 +87,6 @@ module.exports.score = (model, taskId, direction, allowUndo=false) -> delta -### - Make sure model.get() returns all properties, see https://github.com/codeparty/racer/issues/116 -### -module.exports.hydrate = hydrate = (spec) -> - if _.isObject(spec) and !_.isArray(spec) - hydrated = {} - keys = _.keys(spec).concat(_.keys(spec.__proto__)) - keys.forEach (k) -> hydrated[k] = hydrate(spec[k]) - hydrated - else spec - - ### Cleanup task-corruption (null tasks, rogue/invisible tasks, etc) Obviously none of this should be happening, but we'll stop-gap until we can find & fix diff --git a/src/server/api.coffee b/src/server/api.coffee index 8730f0bea7..60121c6af3 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -1,7 +1,7 @@ -express = require 'express' -router = new express.Router() +# @see ./routes.coffee for routing _ = require 'lodash' +async = require 'async' algos = require 'habitrpg-shared/script/algos' helpers = require 'habitrpg-shared/script/helpers' validator = require 'derby-auth/node_modules/validator' @@ -10,9 +10,42 @@ sanitize = validator.sanitize utils = require 'derby-auth/utils' misc = require '../app/misc' +api = module.exports + +### + ------------------------------------------------------------------------ + Misc + ------------------------------------------------------------------------ +#### + NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." +### + beforeEach auth interceptor +### +api.auth = (req, res, next) -> + uid = req.headers['x-api-user'] + token = req.headers['x-api-key'] + return res.json 401, NO_TOKEN_OR_UID unless uid || token + + model = req.getModel() + query = model.query('users').withIdAndToken(uid, token) + + query.fetch (err, user) -> + return res.json err: err if err + (req.habit ?= {}).user = user + req.habit.userObj = user.get() + return res.json 401, NO_USER_FOUND if _.isEmpty(req.habit.userObj) + req._isServer = true + next() + +### + ------------------------------------------------------------------------ + Tasks + ------------------------------------------------------------------------ +### + addTask = (user, task, cb) -> task.type ?= 'habit' tid = user.add "tasks", task, -> @@ -33,20 +66,21 @@ score = (model, user, taskId, direction, cb) -> ### This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login + Export it also so we can call it from deprecated.coffee ### -scoreTask = (req, res, next) -> - {taskId, direction} = req.params +api.scoreTask = (req, res, next) -> + {id, direction} = req.params {title, service, icon, type} = req.body type ||= 'habit' # Send error responses for improper API call - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + return res.json 500, {err: ':id required'} unless id + return res.json 500, {err: ":direction must be 'up' or 'down'"} unless direction in ['up','down'] model = req.getModel() - {user, userObj} = req + {user, userObj} = req.habit - existingTask = user.at "tasks.#{taskId}" + existingTask = user.at "tasks.#{id}" # TODO add service & icon to task # If task exists, set it's compltion if existingTask.get() @@ -54,9 +88,9 @@ scoreTask = (req, res, next) -> existingTask.set 'completed', (direction is 'up') if /^(daily|todo)$/.test existingTask.get('type') else task = - id: taskId + id: id type: type - text: (title || taskId) + text: (title || id) value: 0 notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." @@ -70,129 +104,42 @@ scoreTask = (req, res, next) -> addTask user, task # TODO - could modify batchTxn to conform to this better - delta = score model, user, taskId, direction, -> + delta = score model, user, id, direction, -> result = user.get('stats') - result.delta = delta - res.json result - -# ---------- /api/v1 API ------------ -# Every url added beneath router is prefaced by /api/v1 - -### - v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with: - $ cd node_modules/racer && npm install && cd ../.. - $ mocha test/api.mocha.coffee -### - -### - API Status -### -router.get '/status', (req, res) -> - res.json status: 'up' - -### - beforeEach auth interceptor -### -auth = (req, res, next) -> - uid = req.headers['x-api-user'] - token = req.headers['x-api-key'] - return res.json 401, NO_TOKEN_OR_UID unless uid || token - - model = req.getModel() - query = model.query('users').withIdAndToken(uid, token) - - query.fetch (err, user) -> - return res.json err: err if err - req.user = user - req.userObj = user.get() - return res.json 401, NO_USER_FOUND if _.isEmpty(req.userObj) - req._isServer = true + req.habit.result = data: _.extend(result, delta: delta) next() ### - GET /user + Get all tasks ### -router.get '/user', auth, (req, res) -> - user = req.userObj - - user.stats.toNextLevel = algos.tnl user.stats.lvl - user.stats.maxHealth = 50 - - delete user.apiToken - if user.auth - delete user.auth.hashed_password - delete user.auth.salt - - res.json user +api.getTasks = (req, res, next) -> + return res.json 400, NO_USER_FOUND if _.isEmpty(req.habit.userObj) + types = + if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] + else ['habit','todo','daily','reward'] + tasks = _.toArray (_.filter req.habit.user.get('tasks'), (t)-> t.type in types) + req.habit.result = data: tasks + next() ### - POST /user/auth/local + Get Task ### -router.post '/user/auth/local', (req, res) -> - username = req.body.username - password = req.body.password - return res.json 401, err: 'No username or password' unless username and password - - model = req.getModel() - - q = model.query("users").withUsername(username) - q.fetch (err, result1) -> - return res.json 401, { err } if err - u1 = result1.get() - return res.json 401, err: 'Username not found' unless u1 # user not found - - # We needed the whole user object first so we can get his salt to encrypt password comparison - q = model.query("users").withLogin(username, utils.encryptPassword(password, u1.auth.local.salt)) - q.fetch (err, result2) -> - return res.json 401, { err } if err - - # joshua tree? - u2 = result2.get() - return res.json 401, err: 'Incorrect password' unless u2 - - res.json - id: u2.id - token: u2.apiToken - -### - POST /user/auth/facebook -### -router.post '/user/auth/facebook', (req, res) -> - {facebook_id, email, name} = req.body - return res.json 401, err: 'No facebook id provided' unless facebook_id - model = req.getModel() - q = model.query("users").withProvider('facebook', facebook_id) - q.fetch (err, result) -> - return res.json 401, { err } if err - u = result.get() - console.log {facebook_id, u} - if u - return res.json - id: u.id - token: u.apiToken - else - # FIXME: create a new user instead - return res.json 403, err: "Please register with Facebook on https://habitrpg.com, then come back here and log in." - - -### - GET /user/task/:id -### -router.get '/user/task/:id', auth, (req, res) -> - task = req.userObj.tasks[req.params.id] +api.getTask = (req, res, next) -> + task = req.habit.userObj.tasks[req.params.id] return res.json 400, err: "No task found." if !task || _.isEmpty(task) - res.json 200, task + req.habit.result = data: task + next() ### - validate task + Validate task ### -validateTask = (req, res, next) -> +api.validateTask = (req, res, next) -> task = {} newTask = { type, text, notes, value, up, down, completed } = req.body # If we're updating, get the task from the user if req.method is 'PUT' or req.method is 'DELETE' - task = req.userObj?.tasks[req.params.id] + task = req.habit.userObj?.tasks[req.params.id] return res.json 400, err: "No task found." if !task || _.isEmpty(task) # Strip for now type = undefined @@ -214,25 +161,19 @@ validateTask = (req, res, next) -> newTask.completed = false unless typeof completed is 'boolean' _.extend task, newTask - req.task = task + req.habit.task = task next() ### - PUT /user/task/:id + Delete Task ### -router.put '/user/task/:id', auth, validateTask, (req, res) -> - req.user.set "tasks.#{req.task.id}", req.task - res.json 200, req.task +api.deleteTask = (req, res, next) -> + deleteTask req.habit.user, req.habit.task, -> + req.habit.result = code: 204 + next() ### - DELETE /user/task/:id -### -router.delete '/user/task/:id', auth, validateTask, (req, res) -> - deleteTask req.user, req.task.type, req.task.id - res.send 204 - -### - POST /user/tasks + Helper function for updating multiple tasks ### updateTasks = (tasks, user, model) -> for idx, task of tasks @@ -253,127 +194,191 @@ updateTasks = (tasks, user, model) -> tasks[idx] = task return tasks -router.post '/user/tasks', auth, (req, res) -> - tasks = updateTasks req.body, req.user, req.getModel() - res.json 201, tasks - +### + Update Task +### +api.updateTask = (req, res, next) -> + req.habit.user.set "tasks.#{req.habit.task.id}", req.habit.task + req.habit.result = data: req.habit.task + next() ### - POST /user/task/ + Update tasks (plural). This will update, add new, delete, etc all at once. + Should we keep this? ### -router.post '/user/task', auth, validateTask, (req, res) -> - task = req.task - addTask req.user, task - res.json 201, task +api.updateTasks = (req, res, next) -> + tasks = updateTasks req.body, req.habit.user, req.getModel() + req.habit.result = code: 201, data: tasks + next() + +api.createTask = (req, res, next) -> + task = req.habit.task + addTask req.habit.user, task + req.habit.result = code: 201, data: task + next() + +api.sortTask = (req, res, next) -> + {id} = req.params + {to, from, type} = req.habit.task + {user} = req.habit + path = "#{type}Ids" + a = user.get(path) + a.splice(to, 0, a.splice(from, 1)[0]) + user.set path, a, next ### - GET /user/tasks + ------------------------------------------------------------------------ + User + ------------------------------------------------------------------------ ### -router.get '/user/tasks', auth, (req, res) -> - return res.json 400, NO_USER_FOUND if _.isEmpty(req.userObj) - - types = - if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] - else ['habit','todo','daily','reward'] - tasks = _.toArray (_.filter req.user.get('tasks'), (t)-> t.type in types) - - res.json 200, tasks ### - PUT /user + Get User ### -router.put '/user', auth, (req, res, next) -> +api.getUser = (req, res, next) -> + uObj = req.habit.userObj + + uObj.stats.toNextLevel = algos.tnl uObj.stats.lvl + uObj.stats.maxHealth = 50 + + delete uObj.apiToken + if uObj.auth + delete uObj.auth.hashed_password + delete uObj.auth.salt + + req.habit.result = data: uObj + next() + +### + Register new user with uname / password +### +api.loginLocal = (req, res, next) -> + username = req.body.username + password = req.body.password + return res.json 401, err: 'No username or password' unless username and password + + model = req.getModel() + + q = model.query("users").withUsername(username) + q.fetch (err, result1) -> + return res.json 401, { err } if err + u1 = result1.get() + return res.json 401, err: 'Username not found' unless u1 # user not found + + # We needed the whole user object first so we can get his salt to encrypt password comparison + q = model.query("users").withLogin(username, utils.encryptPassword(password, u1.auth.local.salt)) + q.fetch (err, result2) -> + return res.json 401, { err } if err + + # joshua tree? + u2 = result2.get() + return res.json 401, err: 'Incorrect password' unless u2 + + req.habit ?= {} + req.habit.result = data: + id: u2.id + token: u2.apiToken + next() + +### + POST /user/auth/facebook +### +api.loginFacebook = (req, res, next) -> + {facebook_id, email, name} = req.body + return res.json 401, err: 'No facebook id provided' unless facebook_id + model = req.getModel() + q = model.query("users").withProvider('facebook', facebook_id) + q.fetch (err, result) -> + return res.json 401, { err } if err + u = result.get() + if u + req.habit ?= {} + req.habit.result = data: + id: u.id + token: u.apiToken + next() + else + # FIXME: create a new user instead + return res.json 403, err: "Please register with Facebook on https://habitrpg.com, then come back here and log in." + +### + Update user + FIXME add documentation here +### +api.updateUser = (req, res, next) -> + {user} = req.habit # FIXME we need to do some crazy sanitiazation if they're using the old `PUT /user {data}` method. # The new `PUT /user {'stats.hp':50} # FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations # Note: custom is for 3rd party apps - acceptableAttrs = 'achievements filters flags invitations items lastCron party preferences profile stats tags custom'.join(' ') + acceptableAttrs = 'achievements filters flags invitations items lastCron party preferences profile stats tags custom'.split(' ') series = [] _.each req.body, (v, k) -> if (_.find acceptableAttrs, (attr)-> k.indexOf(attr) is 0)? - series.push (cb) -> req.user.set(k, v, cb) + series.push (cb) -> req.habit.user.set(k, v, cb) async.series series, (err) -> return next(err) if err - res.json 201, helpers.derbyUserToAPI(user) + req.habit.result = data: helpers.derbyUserToAPI(user) + next() + +api.cron = (req, res, next) -> + {user} = req.habit + misc.batchTxn req.getModel(), (uObj, paths) -> + uObj = helpers.derbyUserToAPI(user) + algos.cron uObj, {paths} + , {user, done:next, cron:true} + +api.revive = (req, res, next) -> + {user} = req.habit + [uObj, paths] = [user.get(), {}] + algos.revive uObj, {paths} + setOps = [] + _.each paths, (v,k) -> + setOps.push ((reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb) + async.series setOps, next ### -POST new actions + ------------------------------------------------------------------------ + Batch Update + Run a bunch of updates all at once + ------------------------------------------------------------------------ ### -router.post '/batch-update', auth, (req, res, next) -> - model = req.getModel() - {user} = req +api.batchUpdate = (req, res, next) -> + {user} = req.habit performAction = (action, cb) -> - task = action.task ? {} + req.params.id = action.data?.id + req.params.direction = action.dir + req.body = action.data + switch action.op when "cron" - misc.batchTxn model, (uObj, paths) -> - uObj = helpers.derbyUserToAPI(user) - algos.cron uObj, {paths} - , {user, done:cb, cron:true} - + api.cron(req, res, cb) when "score" - return cb() unless user.get "tasks.#{task.id}" - sendScore = -> score(model, user, task.id, action.dir, cb) - if task.type in ["daily","todo"] - # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir is "up" then true else false - user.set "tasks.#{task.id}.completed", completed, sendScore - else sendScore() - + api.scoreTask(req, res, cb) when "sortTask" - path = action.task.type + "Ids" - a = user.get(path) - a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set path, a, cb - + api.sortTask(req, res, cb) when "addTask" - addTask user, task, cb - + api.validateTask req, res, -> + api.createTask(req, res, cb) when "delTask" - deleteTask user, task, cb - - # this API is only working with string or number variables. It should return error if object given or object is at the path. + api.deleteTask(req, res, cb) when "set" - oldValue = user.get(action.path) - if _.isObject(action.value) or _.isObject(oldValue) - console.error "action.value was an object, which isn't currently supported. Tyler - double check this" - cb() - else - user.set action.path, action.value, cb - + api.updateUser(req, res, cb) when "revive" - [uObj, paths] = [user.get(), {}] - algos.revive uObj, {paths} - setOps = _.map paths, (v,k) -> - (reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb - console.log setOps - async.serial setOps, cb - - else - cb() + api.revive(req, res, cb) + else cb() # Setup the array of functions we're going to call in parallel with async - # Start with cron - (req.body or= []).unshift({op: 'cron'}) - actions = _.transform (req.body), (result, action) -> - result.push (cb) -> performAction(action, cb) unless _.isEmpty(action) + actions = [{op: 'cron'}].concat(_.cloneDeep(req.body) ? []) # Start with cron + actions = _.transform (actions), (result, action) -> + unless _.isEmpty(action) + result.push (cb) -> performAction(action, cb) # call all the operations, then return the user object to the requester async.series actions, (err) -> return next(err) if err res.json 200, helpers.derbyUserToAPI(user) - console.log "Reply sent" - - -### - POST /user/tasks/:taskId/:direction -### -router.post '/user/task/:taskId/:direction', auth, scoreTask -router.post '/user/tasks/:taskId/:direction', auth, scoreTask - -module.exports = router -module.exports.auth = auth -module.exports.scoreTask = scoreTask # export so deprecated can call it \ No newline at end of file + console.log "Reply sent" \ No newline at end of file diff --git a/src/server/index.coffee b/src/server/index.coffee index a723545ae5..b235c949eb 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -76,7 +76,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(store.modelMiddleware()) .use(middleware.translate) # API should be hit before all other routes - .use('/api/v1', require('./api').middleware) + .use('/api/v1', require('./routes').middleware) .use(require('./deprecated').middleware) # Show splash page for newcomers .use(middleware.splash) diff --git a/src/server/routes.coffee b/src/server/routes.coffee new file mode 100644 index 0000000000..9fef8344ef --- /dev/null +++ b/src/server/routes.coffee @@ -0,0 +1,51 @@ +express = require 'express' +router = new express.Router() +api = require './api' + +### + ---------- /api/v1 API ------------ + Every url added to router is prefaced by /api/v1 + See ./routes/coffee for routes + + v1 API. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: + $ cd node_modules/racer && npm install && cd ../.. + $ mocha test/api.mocha.coffee +### + +{auth, validateTask} = api + +### + We don't want the api functions to actually res.send results (unless there was an error) + because we'll be re-using the same functions when apiv2 rolls around, but returning different results. + So handle sending results for apiv1 here +### +v1Send = (req, res, next) -> + {result} = req.habit + if (result.code and result.data) then res.json result.code, result.data + else if result.code then res.send result.code + else if result.data then res.json result.data + else res.send 200 + +router.get '/status', (req, res) -> res.json status: 'up' + +# Scoring +router.post '/user/task/:id/:direction', auth, api.scoreTask, v1Send +router.post '/user/tasks/:id/:direction', auth, api.scoreTask, v1Send + +# Tasks +router.get '/user/tasks', auth, api.getTasks, v1Send # plural +router.get '/user/task/:id', auth, api.getTask, v1Send +router.put '/user/task/:id', auth, validateTask, api.updateTask, v1Send +router.post '/user/tasks', auth, api.updateTasks, v1Send # plural +router.delete '/user/task/:id', auth, validateTask, api.deleteTask, v1Send +router.post '/user/task', auth, validateTask, api.createTask, v1Send +router.put '/user/task/:id/sort', auth, validateTask, api.sortTask, v1Send + +# User +router.get '/user', auth, api.getUser, v1Send +router.post '/user/auth/local', api.loginLocal, v1Send +router.post '/user/auth/facebook', api.loginFacebook, v1Send +router.put '/user', auth, api.updateUser, v1Send +router.post '/user/batch-update', auth, api.batchUpdate # this one we're handling specially + +module.exports = router \ No newline at end of file diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index b0aeec4391..8b0656f6b6 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -379,7 +379,7 @@ describe 'API', -> .send(user: userUpdates) .end (res) -> expect(res.body.err).to.be undefined - expect(res.statusCode).to.be 201 + expect(res.statusCode).to.be 200 tasks = res.body.tasks expect(_.find(tasks,{id:habitId})).to.eql {id: habitId,text: 'hello2',notes: 'note2'} @@ -421,7 +421,7 @@ describe 'API', -> id: userAuth.facebook_id name: userAuth.name email: userAuth.email - console.log {newUser} + #console.log {newUser} model.set "users.#{id}", newUser, -> request.post("#{baseURL}/user/auth/facebook") @@ -434,17 +434,17 @@ describe 'API', -> #expect(res.body.token).to.be newUser.apiToken done() - it.only 'POST /api/v2', (done) -> + it 'PUT /api/v1/batch-update', (done) -> userBefore = {} # user.set "lastCron", +new Date #FIXME this shouldn't be handled here query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) query.fetch (err, user) -> userBefore = user.get() - console.log {userBefore} + #console.log {userBefore} jsonRaw = [{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"up"},{},{},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"up"},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"history":[{"date":1370796966979,"value":-1.9263318037820194},{"date":1371394179245,"value":-9.632667221983818},{"date":1371987764419,"value":-8.899142684639843},{"date":1371901709065,"value":-8.697714139260505},{"date":1371987764419,"value":-9.947389352351902},{"date":1372072605105,"value":-8.65704735207246},{"date":1372185464158,"value":-9.905420946093672},{"date":1372348155721,"value":-8.616465915449927},{"date":1372404365115,"value":-7.369389857231249},{"date":1372619315210,"value":-6.16153655829917},{"date":1372797766170,"value":-4.990495966065229},{"date":1373266931264,"value":-12.356300657493207},{"date":1373322727209,"value":-10.983796434663102},{"date":1373407801484,"value":-9.658725758132753},{"date":1373639117325,"value":-8.377893411957398},{"date":1373719671601,"value":-7.138418159258412},{"date":1373826521297,"value":-5.902428899644443},{"date":1373839445467,"value":-8.264210410818091},{"date":1373929050162,"value":-8.224444248693572},{"date":1374058202835,"value":-9.459055183497052},{"date":1374102966396,"value":-8.184759693251706},{"date":1374187619046,"value":-6.951403643619735},{"date":1374342649005,"value":-5.72148344218024},{"date":1374434356841,"value":-8.072174632205511},{"date":1374562742400,"value":-10.571154014907808},{"date":1374886027789,"value":-16.097395454285916},{"date":1375011848715,"value":-17.607991906162557},{"date":1375436884647,"value":-16.037773949824242},{"date":1375563074478,"value":-17.546064223707074},{"date":1375568230260,"value":-19.11379232897715},{"date":1375734631073,"value":-20.745784396408645},{"date":1375785010434,"value":-18.767794224069412},{"date":1375826853480,"value":-18.636651213045212}],"id":"fe4b9061-eb58-468c-9b25-10c72be772e6","notes":"","price":null,"priority":null,"repeat":{"su":true,"m":true,"t":true,"w":true,"th":true,"f":true,"s":true},"streak":1,"tags":{"40492758-1202-4d85-8cb3-d40e45f4dd1d":false},"text":"Read 50 pages","type":"daily","up":null,"value":-17.024492021142215},"dir":"up"},{},{},{}] - request.post("http://localhost:1337/api/v2") + request.put("http://localhost:1337/api/v1/batch-update") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken)