Merge pull request #2647 from overleaf/em-promisify-history-manager

Promisify HistoryManager

GitOrigin-RevId: cbf946a7d61f7f57bad03e4c83184c6decd91027
This commit is contained in:
Eric Mc Sween 2020-03-03 08:59:09 -05:00 committed by Copybot
parent 1f89083ab2
commit d8d1bdd461
2 changed files with 281 additions and 450 deletions

View file

@ -1,130 +1,75 @@
/* eslint-disable const { callbackify } = require('util')
camelcase, const request = require('request-promise-native')
handle-callback-err,
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const request = require('request')
const settings = require('settings-sharelatex') const settings = require('settings-sharelatex')
const async = require('async') const OError = require('@overleaf/o-error')
const UserGetter = require('../User/UserGetter') const UserGetter = require('../User/UserGetter')
const { promisifyAll } = require('../../util/promises')
const HistoryManager = { module.exports = {
initializeProject(callback) { initializeProject: callbackify(initializeProject),
if (callback == null) { flushProject: callbackify(flushProject),
callback = function(error, history_id) {} resyncProject: callbackify(resyncProject),
injectUserDetails: callbackify(injectUserDetails),
promises: {
initializeProject,
flushProject,
resyncProject,
injectUserDetails
} }
}
async function initializeProject() {
if ( if (
!(settings.apis.project_history != null !(
? settings.apis.project_history.initializeHistoryForNewProjects settings.apis.project_history &&
: undefined) settings.apis.project_history.initializeHistoryForNewProjects
)
) { ) {
return callback() return
} }
return request.post(
{
url: `${settings.apis.project_history.url}/project`
},
function(error, res, body) {
if (error != null) {
return callback(error)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
let project
try { try {
project = JSON.parse(body) const body = await request.post({
} catch (error1) { url: `${settings.apis.project_history.url}/project`
error = error1 })
return callback(error) const project = JSON.parse(body)
const overleafId = project && project.project && project.project.id
if (!overleafId) {
throw new Error('project-history did not provide an id', project)
}
return { overleaf_id: overleafId }
} catch (err) {
throw new OError({
message: 'failed to initialize project history'
}).withCause(err)
}
} }
const overleaf_id = __guard__( async function flushProject(projectId) {
project != null ? project.project : undefined, try {
x => x.id await request.post({
) url: `${settings.apis.project_history.url}/project/${projectId}/flush`
if (!overleaf_id) { })
error = new Error('project-history did not provide an id', project) } catch (err) {
return callback(error) throw new OError({
message: 'failed to flush project to project history',
info: { projectId }
}).withCause(err)
}
} }
return callback(null, { overleaf_id }) async function resyncProject(projectId) {
} else { try {
error = new Error( await request.post({
`project-history returned a non-success status code: ${ url: `${settings.apis.project_history.url}/project/${projectId}/resync`
res.statusCode })
}` } catch (err) {
) throw new OError({
return callback(error) message: 'failed to resync project history',
info: { projectId }
})
} }
} }
)
},
flushProject(project_id, callback) { async function injectUserDetails(data) {
if (callback == null) {
callback = function(error) {}
}
return request.post(
{
url: `${settings.apis.project_history.url}/project/${project_id}/flush`
},
function(error, res, body) {
if (error != null) {
return callback(error)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
return callback()
} else {
error = new Error(
`project-history returned a non-success status code: ${
res.statusCode
}`
)
return callback(error)
}
}
)
},
resyncProject(project_id, callback) {
if (callback == null) {
callback = function(error) {}
}
return request.post(
{
url: `${settings.apis.project_history.url}/project/${project_id}/resync`
},
function(error, res, body) {
if (error != null) {
return callback(error)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
return callback()
} else {
error = new Error(
`project-history returned a non-success status code: ${
res.statusCode
}`
)
return callback(error)
}
}
)
},
injectUserDetails(data, callback) {
// data can be either: // data can be either:
// { // {
// diff: [{ // diff: [{
@ -150,51 +95,37 @@ const HistoryManager = {
// that we need to replace user_ids with populated user objects. // that we need to replace user_ids with populated user objects.
// Note that some entries in the users arrays may be v1 ids returned by the v1 history // Note that some entries in the users arrays may be v1 ids returned by the v1 history
// service. v1 ids will be `numbers` // service. v1 ids will be `numbers`
let entry, user let userIds = new Set()
if (callback == null) { let v1UserIds = new Set()
callback = function(error, data_with_users) {} for (const entry of data.diff || data.updates || []) {
} for (const user of (entry.meta && entry.meta.users) || []) {
let user_ids = new Set()
let v1_user_ids = new Set()
for (entry of Array.from(data.diff || data.updates || [])) {
for (user of Array.from(
(entry.meta != null ? entry.meta.users : undefined) || []
)) {
if (typeof user === 'string') { if (typeof user === 'string') {
user_ids.add(user) userIds.add(user)
} else if (typeof user === 'number') { } else if (typeof user === 'number') {
v1_user_ids.add(user) v1UserIds.add(user)
} }
} }
} }
user_ids = Array.from(user_ids)
v1_user_ids = Array.from(v1_user_ids) userIds = Array.from(userIds)
v1UserIds = Array.from(v1UserIds)
const projection = { first_name: 1, last_name: 1, email: 1 } const projection = { first_name: 1, last_name: 1, email: 1 }
return UserGetter.getUsers(user_ids, projection, function( const usersArray = await UserGetter.promises.getUsers(userIds, projection)
error,
users_array
) {
if (error != null) {
return callback(error)
}
const v1_query = { 'overleaf.id': v1_user_ids }
const users = {} const users = {}
for (user of Array.from(users_array || [])) { for (const user of usersArray) {
users[user._id.toString()] = HistoryManager._userView(user) users[user._id.toString()] = _userView(user)
} }
projection.overleaf = 1 projection.overleaf = 1
UserGetter.getUsersByV1Ids( const v1IdentifiedUsersArray = await UserGetter.promises.getUsersByV1Ids(
v1_user_ids, v1UserIds,
projection, projection
(error, v1_identified_users_array) => { )
for (user of Array.from(v1_identified_users_array || [])) { for (const user of v1IdentifiedUsersArray) {
users[user.overleaf.id] = HistoryManager._userView(user) users[user.overleaf.id] = _userView(user)
} }
for (entry of Array.from(data.diff || data.updates || [])) { for (const entry of data.diff || data.updates || []) {
if (entry.meta != null) { if (entry.meta != null) {
entry.meta.users = ( entry.meta.users = ((entry.meta && entry.meta.users) || []).map(user => {
(entry.meta != null ? entry.meta.users : undefined) || []
).map(function(user) {
if (typeof user === 'string' || typeof user === 'number') { if (typeof user === 'string' || typeof user === 'number') {
return users[user] return users[user]
} else { } else {
@ -203,22 +134,10 @@ const HistoryManager = {
}) })
} }
} }
return callback(null, data) return data
}
)
})
},
_userView(user) {
const { _id, first_name, last_name, email } = user
return { first_name, last_name, email, id: _id }
}
}
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
} }
HistoryManager.promises = promisifyAll(HistoryManager, { without: '_userView' }) function _userView(user) {
module.exports = HistoryManager const { _id, first_name: firstName, last_name: lastName, email } = user
return { first_name: firstName, last_name: lastName, email, id: _id }
}

View file

@ -1,39 +1,20 @@
/* eslint-disable const { expect } = require('chai')
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const chai = require('chai')
chai.should()
const { expect } = chai
const sinon = require('sinon') const sinon = require('sinon')
const modulePath = '../../../../app/src/Features/History/HistoryManager'
const SandboxedModule = require('sandboxed-module') const SandboxedModule = require('sandboxed-module')
const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager'
describe('HistoryManager', function() { describe('HistoryManager', function() {
beforeEach(function() { beforeEach(function() {
this.callback = sinon.stub()
this.user_id = 'user-id-123' this.user_id = 'user-id-123'
this.AuthenticationController = { this.AuthenticationController = {
getLoggedInUserId: sinon.stub().returns(this.user_id) getLoggedInUserId: sinon.stub().returns(this.user_id)
} }
this.HistoryManager = SandboxedModule.require(modulePath, { this.request = {
globals: { post: sinon.stub()
console: console
},
requires: {
request: (this.request = sinon.stub()),
'settings-sharelatex': (this.settings = {}),
'../User/UserGetter': (this.UserGetter = {})
} }
}) this.settings = {
return (this.settings.apis = { apis: {
trackchanges: { trackchanges: {
enabled: false, enabled: false,
url: 'http://trackchanges.example.com' url: 'http://trackchanges.example.com'
@ -41,107 +22,78 @@ describe('HistoryManager', function() {
project_history: { project_history: {
url: 'http://project_history.example.com' url: 'http://project_history.example.com'
} }
}
}
this.UserGetter = {
promises: {
getUsersByV1Ids: sinon.stub(),
getUsers: sinon.stub()
}
}
this.HistoryManager = SandboxedModule.require(MODULE_PATH, {
globals: {
console: console
},
requires: {
'request-promise-native': this.request,
'settings-sharelatex': this.settings,
'../User/UserGetter': this.UserGetter
}
}) })
}) })
describe('initializeProject', function() { describe('initializeProject', function() {
describe('with project history enabled', function() { describe('with project history enabled', function() {
beforeEach(function() { beforeEach(function() {
return (this.settings.apis.project_history.initializeHistoryForNewProjects = true) this.settings.apis.project_history.initializeHistoryForNewProjects = true
}) })
describe('project history returns a successful response', function() { describe('project history returns a successful response', function() {
beforeEach(function() { beforeEach(async function() {
this.overleaf_id = 1234 this.overleaf_id = 1234
this.res = { statusCode: 200 } this.request.post.resolves(
this.body = JSON.stringify({ project: { id: this.overleaf_id } }) JSON.stringify({ project: { id: this.overleaf_id } })
this.request.post = sinon )
.stub() this.result = await this.HistoryManager.promises.initializeProject()
.callsArgWith(1, null, this.res, this.body)
return this.HistoryManager.initializeProject(this.callback)
}) })
it('should call the project history api', function() { it('should call the project history api', function() {
return this.request.post this.request.post
.calledWith({ .calledWith({
url: `${this.settings.apis.project_history.url}/project` url: `${this.settings.apis.project_history.url}/project`
}) })
.should.equal(true) .should.equal(true)
}) })
it('should return the callback with the overleaf id', function() { it('should return the overleaf id', function() {
return this.callback expect(this.result).to.deep.equal({ overleaf_id: this.overleaf_id })
.calledWithExactly(null, { overleaf_id: this.overleaf_id })
.should.equal(true)
}) })
}) })
describe('project history returns a response without the project id', function() { describe('project history returns a response without the project id', function() {
beforeEach(function() { it('should throw an error', async function() {
this.res = { statusCode: 200 } this.request.post.resolves(JSON.stringify({ project: {} }))
this.body = JSON.stringify({ project: {} }) await expect(this.HistoryManager.promises.initializeProject()).to.be
this.request.post = sinon .rejected
.stub()
.callsArgWith(1, null, this.res, this.body)
return this.HistoryManager.initializeProject(this.callback)
})
it('should return the callback with an error', function() {
return this.callback
.calledWith(
sinon.match.has(
'message',
'project-history did not provide an id'
)
)
.should.equal(true)
})
})
describe('project history returns a unsuccessful response', function() {
beforeEach(function() {
this.res = { statusCode: 404 }
this.request.post = sinon.stub().callsArgWith(1, null, this.res)
return this.HistoryManager.initializeProject(this.callback)
})
it('should return the callback with an error', function() {
return this.callback
.calledWith(
sinon.match.has(
'message',
'project-history returned a non-success status code: 404'
)
)
.should.equal(true)
}) })
}) })
describe('project history errors', function() { describe('project history errors', function() {
beforeEach(function() { it('should propagate the error', async function() {
this.error = sinon.stub() this.request.post.rejects(new Error('problem connecting'))
this.request.post = sinon.stub().callsArgWith(1, this.error) await expect(this.HistoryManager.promises.initializeProject()).to.be
.rejected
return this.HistoryManager.initializeProject(this.callback)
})
it('should return the callback with the error', function() {
return this.callback.calledWithExactly(this.error).should.equal(true)
}) })
}) })
}) })
describe('with project history disabled', function() { describe('with project history disabled', function() {
beforeEach(function() { it('should return without errors', async function() {
this.settings.apis.project_history.initializeHistoryForNewProjects = false this.settings.apis.project_history.initializeHistoryForNewProjects = false
return this.HistoryManager.initializeProject(this.callback) await expect(this.HistoryManager.promises.initializeProject()).to.be
}) .fulfilled
it('should return the callback', function() {
return this.callback.calledWithExactly().should.equal(true)
}) })
}) })
}) })
@ -173,16 +125,13 @@ describe('HistoryManager', function() {
last_name: 'Doe', last_name: 'Doe',
email: 'john@example.com' email: 'john@example.com'
} }
this.UserGetter.getUsersByV1Ids = sinon.stub().yields(null, [this.user1]) this.UserGetter.promises.getUsersByV1Ids.resolves([this.user1])
return (this.UserGetter.getUsers = sinon this.UserGetter.promises.getUsers.resolves([this.user1, this.user2])
.stub()
.yields(null, [this.user1, this.user2]))
}) })
describe('with a diff', function() { describe('with a diff', function() {
it('should turn user_ids into user objects', function(done) { it('should turn user_ids into user objects', async function() {
return this.HistoryManager.injectUserDetails( const diff = await this.HistoryManager.promises.injectUserDetails({
{
diff: [ diff: [
{ {
i: 'foo', i: 'foo',
@ -197,19 +146,13 @@ describe('HistoryManager', function() {
} }
} }
] ]
}, })
(error, diff) => {
expect(error).to.be.null
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view])
return done()
}
)
}) })
it('should handle v1 user ids', function(done) { it('should handle v1 user ids', async function() {
return this.HistoryManager.injectUserDetails( const diff = await this.HistoryManager.promises.injectUserDetails({
{
diff: [ diff: [
{ {
i: 'foo', i: 'foo',
@ -224,19 +167,13 @@ describe('HistoryManager', function() {
} }
} }
] ]
}, })
(error, diff) => {
expect(error).to.be.null
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view])
return done()
}
)
}) })
it('should leave user objects', function(done) { it('should leave user objects', async function() {
return this.HistoryManager.injectUserDetails( const diff = await this.HistoryManager.promises.injectUserDetails({
{
diff: [ diff: [
{ {
i: 'foo', i: 'foo',
@ -251,21 +188,15 @@ describe('HistoryManager', function() {
} }
} }
] ]
}, })
(error, diff) => {
expect(error).to.be.null
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view])
return done()
}
)
}) })
}) })
describe('with a list of updates', function() { describe('with a list of updates', function() {
it('should turn user_ids into user objects', function(done) { it('should turn user_ids into user objects', async function() {
return this.HistoryManager.injectUserDetails( const updates = await this.HistoryManager.promises.injectUserDetails({
{
updates: [ updates: [
{ {
fromV: 5, fromV: 5,
@ -282,23 +213,13 @@ describe('HistoryManager', function() {
} }
} }
] ]
}, })
(error, updates) => { expect(updates.updates[0].meta.users).to.deep.equal([this.user1_view])
expect(error).to.be.null expect(updates.updates[1].meta.users).to.deep.equal([this.user2_view])
expect(updates.updates[0].meta.users).to.deep.equal([
this.user1_view
])
expect(updates.updates[1].meta.users).to.deep.equal([
this.user2_view
])
return done()
}
)
}) })
it('should leave user objects', function(done) { it('should leave user objects', async function() {
return this.HistoryManager.injectUserDetails( const updates = await this.HistoryManager.promises.injectUserDetails({
{
updates: [ updates: [
{ {
fromV: 5, fromV: 5,
@ -315,18 +236,9 @@ describe('HistoryManager', function() {
} }
} }
] ]
}, })
(error, updates) => { expect(updates.updates[0].meta.users).to.deep.equal([this.user1_view])
expect(error).to.be.null expect(updates.updates[1].meta.users).to.deep.equal([this.user2_view])
expect(updates.updates[0].meta.users).to.deep.equal([
this.user1_view
])
expect(updates.updates[1].meta.users).to.deep.equal([
this.user2_view
])
return done()
}
)
}) })
}) })
}) })