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,224 +1,143 @@
/* 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),
if ( promises: {
!(settings.apis.project_history != null initializeProject,
? settings.apis.project_history.initializeHistoryForNewProjects flushProject,
: undefined) resyncProject,
) { injectUserDetails
return callback()
}
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 {
project = JSON.parse(body)
} catch (error1) {
error = error1
return callback(error)
}
const overleaf_id = __guard__(
project != null ? project.project : undefined,
x => x.id
)
if (!overleaf_id) {
error = new Error('project-history did not provide an id', project)
return callback(error)
}
return callback(null, { overleaf_id })
} else {
error = new Error(
`project-history returned a non-success status code: ${
res.statusCode
}`
)
return callback(error)
}
}
)
},
flushProject(project_id, callback) {
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:
// {
// diff: [{
// i: "foo",
// meta: {
// users: ["user_id", v1_user_id, ...]
// ...
// }
// }, ...]
// }
// or
// {
// updates: [{
// pathnames: ["main.tex"]
// meta: {
// users: ["user_id", v1_user_id, ...]
// ...
// },
// ...
// }, ...]
// }
// Either way, the top level key points to an array of objects with a meta.users property
// 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
// service. v1 ids will be `numbers`
let entry, user
if (callback == null) {
callback = function(error, data_with_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') {
user_ids.add(user)
} else if (typeof user === 'number') {
v1_user_ids.add(user)
}
}
}
user_ids = Array.from(user_ids)
v1_user_ids = Array.from(v1_user_ids)
const projection = { first_name: 1, last_name: 1, email: 1 }
return UserGetter.getUsers(user_ids, projection, function(
error,
users_array
) {
if (error != null) {
return callback(error)
}
const v1_query = { 'overleaf.id': v1_user_ids }
const users = {}
for (user of Array.from(users_array || [])) {
users[user._id.toString()] = HistoryManager._userView(user)
}
projection.overleaf = 1
UserGetter.getUsersByV1Ids(
v1_user_ids,
projection,
(error, v1_identified_users_array) => {
for (user of Array.from(v1_identified_users_array || [])) {
users[user.overleaf.id] = HistoryManager._userView(user)
}
for (entry of Array.from(data.diff || data.updates || [])) {
if (entry.meta != null) {
entry.meta.users = (
(entry.meta != null ? entry.meta.users : undefined) || []
).map(function(user) {
if (typeof user === 'string' || typeof user === 'number') {
return users[user]
} else {
return user
}
})
}
}
return callback(null, 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 async function initializeProject() {
? transform(value) if (
: undefined !(
settings.apis.project_history &&
settings.apis.project_history.initializeHistoryForNewProjects
)
) {
return
}
try {
const body = await request.post({
url: `${settings.apis.project_history.url}/project`
})
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)
}
} }
HistoryManager.promises = promisifyAll(HistoryManager, { without: '_userView' }) async function flushProject(projectId) {
module.exports = HistoryManager try {
await request.post({
url: `${settings.apis.project_history.url}/project/${projectId}/flush`
})
} catch (err) {
throw new OError({
message: 'failed to flush project to project history',
info: { projectId }
}).withCause(err)
}
}
async function resyncProject(projectId) {
try {
await request.post({
url: `${settings.apis.project_history.url}/project/${projectId}/resync`
})
} catch (err) {
throw new OError({
message: 'failed to resync project history',
info: { projectId }
})
}
}
async function injectUserDetails(data) {
// data can be either:
// {
// diff: [{
// i: "foo",
// meta: {
// users: ["user_id", v1_user_id, ...]
// ...
// }
// }, ...]
// }
// or
// {
// updates: [{
// pathnames: ["main.tex"]
// meta: {
// users: ["user_id", v1_user_id, ...]
// ...
// },
// ...
// }, ...]
// }
// Either way, the top level key points to an array of objects with a meta.users property
// 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
// service. v1 ids will be `numbers`
let userIds = new Set()
let v1UserIds = new Set()
for (const entry of data.diff || data.updates || []) {
for (const user of (entry.meta && entry.meta.users) || []) {
if (typeof user === 'string') {
userIds.add(user)
} else if (typeof user === 'number') {
v1UserIds.add(user)
}
}
}
userIds = Array.from(userIds)
v1UserIds = Array.from(v1UserIds)
const projection = { first_name: 1, last_name: 1, email: 1 }
const usersArray = await UserGetter.promises.getUsers(userIds, projection)
const users = {}
for (const user of usersArray) {
users[user._id.toString()] = _userView(user)
}
projection.overleaf = 1
const v1IdentifiedUsersArray = await UserGetter.promises.getUsersByV1Ids(
v1UserIds,
projection
)
for (const user of v1IdentifiedUsersArray) {
users[user.overleaf.id] = _userView(user)
}
for (const entry of data.diff || data.updates || []) {
if (entry.meta != null) {
entry.meta.users = ((entry.meta && entry.meta.users) || []).map(user => {
if (typeof user === 'string' || typeof user === 'number') {
return users[user]
} else {
return user
}
})
}
}
return data
}
function _userView(user) {
const { _id, first_name: firstName, last_name: lastName, email } = user
return { first_name: firstName, last_name: lastName, email, id: _id }
}

View file

@ -1,45 +1,45 @@
/* 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 = {
post: sinon.stub()
}
this.settings = {
apis: {
trackchanges: {
enabled: false,
url: 'http://trackchanges.example.com'
},
project_history: {
url: 'http://project_history.example.com'
}
}
}
this.UserGetter = {
promises: {
getUsersByV1Ids: sinon.stub(),
getUsers: sinon.stub()
}
}
this.HistoryManager = SandboxedModule.require(MODULE_PATH, {
globals: { globals: {
console: console console: console
}, },
requires: { requires: {
request: (this.request = sinon.stub()), 'request-promise-native': this.request,
'settings-sharelatex': (this.settings = {}), 'settings-sharelatex': this.settings,
'../User/UserGetter': (this.UserGetter = {}) '../User/UserGetter': this.UserGetter
}
})
return (this.settings.apis = {
trackchanges: {
enabled: false,
url: 'http://trackchanges.example.com'
},
project_history: {
url: 'http://project_history.example.com'
} }
}) })
}) })
@ -47,101 +47,53 @@ describe('HistoryManager', function() {
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,160 +125,120 @@ 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', meta: {
meta: { users: [this.user_id1]
users: [this.user_id1]
}
},
{
i: 'bar',
meta: {
users: [this.user_id2]
}
} }
] },
}, {
(error, diff) => { i: 'bar',
expect(error).to.be.null meta: {
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) users: [this.user_id2]
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) }
return done() }
} ]
) })
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view])
}) })
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', meta: {
meta: { users: [5011]
users: [5011]
}
},
{
i: 'bar',
meta: {
users: [this.user_id2]
}
} }
] },
}, {
(error, diff) => { i: 'bar',
expect(error).to.be.null meta: {
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) users: [this.user_id2]
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) }
return done() }
} ]
) })
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view])
}) })
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', meta: {
meta: { users: [this.user1_view]
users: [this.user1_view]
}
},
{
i: 'bar',
meta: {
users: [this.user_id2]
}
} }
] },
}, {
(error, diff) => { i: 'bar',
expect(error).to.be.null meta: {
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) users: [this.user_id2]
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) }
return done() }
} ]
) })
expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view])
}) })
}) })
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, toV: 8,
toV: 8, meta: {
meta: { users: [this.user_id1]
users: [this.user_id1]
}
},
{
fromV: 4,
toV: 5,
meta: {
users: [this.user_id2]
}
} }
] },
}, {
(error, updates) => { fromV: 4,
expect(error).to.be.null toV: 5,
expect(updates.updates[0].meta.users).to.deep.equal([ meta: {
this.user1_view users: [this.user_id2]
]) }
expect(updates.updates[1].meta.users).to.deep.equal([ }
this.user2_view ]
]) })
return done() expect(updates.updates[0].meta.users).to.deep.equal([this.user1_view])
} expect(updates.updates[1].meta.users).to.deep.equal([this.user2_view])
)
}) })
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, toV: 8,
toV: 8, meta: {
meta: { users: [this.user1_view]
users: [this.user1_view]
}
},
{
fromV: 4,
toV: 5,
meta: {
users: [this.user_id2]
}
} }
] },
}, {
(error, updates) => { fromV: 4,
expect(error).to.be.null toV: 5,
expect(updates.updates[0].meta.users).to.deep.equal([ meta: {
this.user1_view users: [this.user_id2]
]) }
expect(updates.updates[1].meta.users).to.deep.equal([ }
this.user2_view ]
]) })
return done() expect(updates.updates[0].meta.users).to.deep.equal([this.user1_view])
} expect(updates.updates[1].meta.users).to.deep.equal([this.user2_view])
)
}) })
}) })
}) })