Add HTTP end point for accepting changes

This commit is contained in:
James Allen 2017-01-09 14:41:18 +01:00
parent 65f4360738
commit be19532a1d
8 changed files with 187 additions and 7 deletions

View file

@ -45,6 +45,7 @@ app.post '/project/:project_id/doc/:doc_id/flush', HttpController.flushDocIfLo
app.delete '/project/:project_id/doc/:doc_id', HttpController.flushAndDeleteDoc app.delete '/project/:project_id/doc/:doc_id', HttpController.flushAndDeleteDoc
app.delete '/project/:project_id', HttpController.deleteProject app.delete '/project/:project_id', HttpController.deleteProject
app.post '/project/:project_id/flush', HttpController.flushProject app.post '/project/:project_id/flush', HttpController.flushProject
app.post '/project/:project_id/doc/:doc_id/change/:change_id/accept', HttpController.acceptChange
app.get '/total', (req, res)-> app.get '/total', (req, res)->
timer = new Metrics.Timer("http.allDocList") timer = new Metrics.Timer("http.allDocList")

View file

@ -5,6 +5,8 @@ logger = require "logger-sharelatex"
Metrics = require "./Metrics" Metrics = require "./Metrics"
HistoryManager = require "./HistoryManager" HistoryManager = require "./HistoryManager"
WebRedisManager = require "./WebRedisManager" WebRedisManager = require "./WebRedisManager"
Errors = require "./Errors"
RangesManager = require "./RangesManager"
module.exports = DocumentManager = module.exports = DocumentManager =
getDoc: (project_id, doc_id, _callback = (error, lines, version, alreadyLoaded) ->) -> getDoc: (project_id, doc_id, _callback = (error, lines, version, alreadyLoaded) ->) ->
@ -84,7 +86,6 @@ module.exports = DocumentManager =
return callback(error) if error? return callback(error) if error?
callback null callback null
flushDocIfLoaded: (project_id, doc_id, _callback = (error) ->) -> flushDocIfLoaded: (project_id, doc_id, _callback = (error) ->) ->
timer = new Metrics.Timer("docManager.flushDocIfLoaded") timer = new Metrics.Timer("docManager.flushDocIfLoaded")
callback = (args...) -> callback = (args...) ->
@ -120,6 +121,22 @@ module.exports = DocumentManager =
return callback(error) if error? return callback(error) if error?
callback null callback null
acceptChange: (project_id, doc_id, change_id, _callback = (error) ->) ->
timer = new Metrics.Timer("docManager.acceptChange")
callback = (args...) ->
timer.done()
_callback(args...)
DocumentManager.getDoc project_id, doc_id, (error, lines, version, ranges) ->
return callback(error) if error?
if !lines? or !version?
return callback(new Errors.NotFoundError("document not found: #{doc_id}"))
RangesManager.acceptChange change_id, ranges, (error, new_ranges) ->
return callback(error) if error?
RedisManager.updateDocument doc_id, lines, version, [], new_ranges, (error) ->
return callback(error) if error?
callback()
getDocWithLock: (project_id, doc_id, callback = (error, lines, version) ->) -> getDocWithLock: (project_id, doc_id, callback = (error, lines, version) ->) ->
UpdateManager = require "./UpdateManager" UpdateManager = require "./UpdateManager"
UpdateManager.lockUpdatesAndDo DocumentManager.getDoc, project_id, doc_id, callback UpdateManager.lockUpdatesAndDo DocumentManager.getDoc, project_id, doc_id, callback
@ -139,3 +156,7 @@ module.exports = DocumentManager =
flushAndDeleteDocWithLock: (project_id, doc_id, callback = (error) ->) -> flushAndDeleteDocWithLock: (project_id, doc_id, callback = (error) ->) ->
UpdateManager = require "./UpdateManager" UpdateManager = require "./UpdateManager"
UpdateManager.lockUpdatesAndDo DocumentManager.flushAndDeleteDoc, project_id, doc_id, callback UpdateManager.lockUpdatesAndDo DocumentManager.flushAndDeleteDoc, project_id, doc_id, callback
acceptChangeWithLock: (project_id, doc_id, change_id, callback = (error) ->) ->
UpdateManager = require "./UpdateManager"
UpdateManager.lockUpdatesAndDo DocumentManager.acceptChange, project_id, doc_id, change_id, callback

View file

@ -98,3 +98,14 @@ module.exports = HttpController =
logger.log project_id: project_id, "deleted project via http" logger.log project_id: project_id, "deleted project via http"
res.send 204 # No Content res.send 204 # No Content
acceptChange: (req, res, next = (error) ->) ->
{project_id, doc_id, change_id} = req.params
logger.log {project_id, doc_id, change_id}, "accepting change via http"
timer = new Metrics.Timer("http.acceptChange")
DocumentManager.acceptChangeWithLock project_id, doc_id, change_id, (error) ->
timer.done()
return next(error) if error?
logger.log {project_id, doc_id, change_id}, "accepted change via http"
res.send 204 # No Content

View file

@ -4,7 +4,7 @@ logger = require "logger-sharelatex"
module.exports = RangesManager = module.exports = RangesManager =
applyUpdate: (project_id, doc_id, entries = {}, updates = [], callback = (error, new_entries) ->) -> applyUpdate: (project_id, doc_id, entries = {}, updates = [], callback = (error, new_entries) ->) ->
{changes, comments} = entries {changes, comments} = entries
logger.log {changes, comments, updates}, "appliyng updates to ranges" logger.log {changes, comments, updates}, "applying updates to ranges"
rangesTracker = new RangesTracker(changes, comments) rangesTracker = new RangesTracker(changes, comments)
for update in updates for update in updates
rangesTracker.track_changes = !!update.meta.tc rangesTracker.track_changes = !!update.meta.tc
@ -13,6 +13,19 @@ module.exports = RangesManager =
for op in update.op for op in update.op
rangesTracker.applyOp(op, { user_id: update.meta?.user_id }) rangesTracker.applyOp(op, { user_id: update.meta?.user_id })
response = RangesManager._getRanges rangesTracker
logger.log {response}, "applied updates to ranges"
callback null, response
acceptChange: (change_id, ranges, callback = (error, ranges) ->) ->
{changes, comments} = ranges
logger.log {changes, comments, change_id}, "accepting change in ranges"
rangesTracker = new RangesTracker(changes, comments)
rangesTracker.removeChangeId(change_id)
response = RangesManager._getRanges(rangesTracker)
callback null, response
_getRanges: (rangesTracker) ->
# Return the minimal data structure needed, since most documents won't have any # Return the minimal data structure needed, since most documents won't have any
# changes or comments # changes or comments
response = {} response = {}
@ -22,5 +35,4 @@ module.exports = RangesManager =
if rangesTracker.comments?.length > 0 if rangesTracker.comments?.length > 0
response ?= {} response ?= {}
response.comments = rangesTracker.comments response.comments = rangesTracker.comments
logger.log {response}, "applied updates to ranges" return response
callback null, response

View file

@ -1,6 +1,7 @@
sinon = require "sinon" sinon = require "sinon"
chai = require("chai") chai = require("chai")
chai.should() chai.should()
expect = chai.expect
async = require "async" async = require "async"
rclient = require("redis").createClient() rclient = require("redis").createClient()
@ -182,3 +183,45 @@ describe "Ranges", ->
changes[0].op.should.deep.equal { i: "123", p: 1 } changes[0].op.should.deep.equal { i: "123", p: 1 }
changes[1].op.should.deep.equal { i: "456", p: 5 } changes[1].op.should.deep.equal { i: "456", p: 5 }
done() done()
describe "accepting a change", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@id_seed = "587357bd35e64f6157"
@doc = {
id: DocUpdaterClient.randomId()
lines: ["aaa"]
}
@update = {
doc: @doc.id
op: [{ i: "456", p: 1 }]
v: 0
meta: { user_id: @user_id, tc: @id_seed }
}
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
version: 0
}
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc.id, @update, (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
ranges = data.ranges
change = ranges.changes[0]
change.op.should.deep.equal { i: "456", p: 1 }
change.id.should.equal @id_seed + "000001"
change.metadata.user_id.should.equal @user_id
done()
, 200
it "should remove the change after accepting", (done) ->
DocUpdaterClient.acceptChange @project_id, @doc.id, @id_seed + "000001", (error) =>
throw error if error?
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
expect(data.ranges.changes).to.be.undefined
done()

View file

@ -72,3 +72,6 @@ module.exports = DocUpdaterClient =
deleteProject: (project_id, callback = () ->) -> deleteProject: (project_id, callback = () ->) ->
request.del "http://localhost:3003/project/#{project_id}", callback request.del "http://localhost:3003/project/#{project_id}", callback
acceptChange: (project_id, doc_id, change_id, callback = () ->) ->
request.post "http://localhost:3003/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept", callback

View file

@ -3,6 +3,7 @@ chai = require('chai')
should = chai.should() should = chai.should()
modulePath = "../../../../app/js/DocumentManager.js" modulePath = "../../../../app/js/DocumentManager.js"
SandboxedModule = require('sandboxed-module') SandboxedModule = require('sandboxed-module')
Errors = require "../../../../app/js/Errors"
describe "DocumentManager", -> describe "DocumentManager", ->
beforeEach -> beforeEach ->
@ -18,6 +19,7 @@ describe "DocumentManager", ->
"./WebRedisManager": @WebRedisManager = {} "./WebRedisManager": @WebRedisManager = {}
"./DiffCodec": @DiffCodec = {} "./DiffCodec": @DiffCodec = {}
"./UpdateManager": @UpdateManager = {} "./UpdateManager": @UpdateManager = {}
"./RangesManager": @RangesManager = {}
@project_id = "project-id-123" @project_id = "project-id-123"
@doc_id = "doc-id-123" @doc_id = "doc-id-123"
@callback = sinon.stub() @callback = sinon.stub()
@ -260,3 +262,49 @@ describe "DocumentManager", ->
it "should not try to get the doc lines", -> it "should not try to get the doc lines", ->
@DocumentManager.getDoc.called.should.equal false @DocumentManager.getDoc.called.should.equal false
describe "acceptChanges", ->
beforeEach ->
@change_id = "mock-change-id"
@version = 34
@lines = ["original", "lines"]
@ranges = { entries: "mock", comments: "mock" }
@updated_ranges = { entries: "updated", comments: "updated" }
@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
@RangesManager.acceptChange = sinon.stub().yields(null, @updated_ranges)
@RedisManager.updateDocument = sinon.stub().yields()
describe "successfully", ->
beforeEach ->
@DocumentManager.acceptChange @project_id, @doc_id, @change_id, @callback
it "should get the document's current ranges", ->
@DocumentManager.getDoc
.calledWith(@project_id, @doc_id)
.should.equal true
it "should apply the accept change to the ranges", ->
@RangesManager.acceptChange
.calledWith(@change_id, @ranges)
.should.equal true
it "should save the updated ranges", ->
@RedisManager.updateDocument
.calledWith(@doc_id, @lines, @version, [], @updated_ranges)
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe "when the doc is not found", ->
beforeEach ->
@DocumentManager.getDoc = sinon.stub().yields(null, null, null, null)
@DocumentManager.acceptChange @project_id, @doc_id, @change_id, @callback
it "should not save anything", ->
@RedisManager.updateDocument.called.should.equal false
it "should call the callback with a not found error", ->
error = new Errors.NotFoundError("document not found: #{@doc_id}")
@callback.calledWith(error).should.equal true

View file

@ -333,3 +333,44 @@ describe "HttpController", ->
@next @next
.calledWith(new Error("oops")) .calledWith(new Error("oops"))
.should.equal true .should.equal true
describe "acceptChange", ->
beforeEach ->
@req =
params:
project_id: @project_id
doc_id: @doc_id
change_id: @change_id = "mock-change-od-1"
describe "successfully", ->
beforeEach ->
@DocumentManager.acceptChangeWithLock = sinon.stub().callsArgWith(3)
@HttpController.acceptChange(@req, @res, @next)
it "should accept the change", ->
@DocumentManager.acceptChangeWithLock
.calledWith(@project_id, @doc_id, @change_id)
.should.equal true
it "should return a successful No Content response", ->
@res.send
.calledWith(204)
.should.equal true
it "should log the request", ->
@logger.log
.calledWith({@project_id, @doc_id, @change_id}, "accepting change via http")
.should.equal true
it "should time the request", ->
@Metrics.Timer::done.called.should.equal true
describe "when an errors occurs", ->
beforeEach ->
@DocumentManager.acceptChangeWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
@HttpController.acceptChange(@req, @res, @next)
it "should call next with the error", ->
@next
.calledWith(new Error("oops"))
.should.equal true