DocManagerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { expect } = require('chai')
  4. const modulePath = require('path').join(__dirname, '../../../app/js/DocManager')
  5. const { ObjectId } = require('mongodb-legacy')
  6. const Errors = require('../../../app/js/Errors')
  7. describe('DocManager', function () {
  8. beforeEach(function () {
  9. this.doc_id = new ObjectId().toString()
  10. this.project_id = new ObjectId().toString()
  11. this.another_project_id = new ObjectId().toString()
  12. this.stubbedError = new Error('blew up')
  13. this.version = 42
  14. this.MongoManager = {
  15. promises: {
  16. findDoc: sinon.stub(),
  17. getProjectsDocs: sinon.stub(),
  18. patchDoc: sinon.stub().resolves(),
  19. upsertIntoDocCollection: sinon.stub().resolves(),
  20. },
  21. }
  22. this.DocArchiveManager = {
  23. promises: {
  24. unarchiveDoc: sinon.stub(),
  25. unArchiveAllDocs: sinon.stub(),
  26. archiveDoc: sinon.stub().resolves(),
  27. },
  28. }
  29. this.RangeManager = {
  30. jsonRangesToMongo(r) {
  31. return r
  32. },
  33. shouldUpdateRanges: sinon.stub().returns(false),
  34. }
  35. this.settings = { docstore: {} }
  36. this.DocManager = SandboxedModule.require(modulePath, {
  37. requires: {
  38. './MongoManager': this.MongoManager,
  39. './DocArchiveManager': this.DocArchiveManager,
  40. './RangeManager': this.RangeManager,
  41. '@overleaf/settings': this.settings,
  42. './Errors': Errors,
  43. },
  44. })
  45. })
  46. describe('getFullDoc', function () {
  47. beforeEach(function () {
  48. this.DocManager.promises._getDoc = sinon.stub()
  49. this.doc = {
  50. _id: this.doc_id,
  51. lines: ['2134'],
  52. }
  53. })
  54. it('should call get doc with a quick filter', async function () {
  55. this.DocManager.promises._getDoc.resolves(this.doc)
  56. const doc = await this.DocManager.promises.getFullDoc(
  57. this.project_id,
  58. this.doc_id
  59. )
  60. doc.should.equal(this.doc)
  61. this.DocManager.promises._getDoc
  62. .calledWith(this.project_id, this.doc_id, {
  63. lines: true,
  64. rev: true,
  65. deleted: true,
  66. version: true,
  67. ranges: true,
  68. inS3: true,
  69. })
  70. .should.equal(true)
  71. })
  72. it('should return error when get doc errors', async function () {
  73. this.DocManager.promises._getDoc.rejects(this.stubbedError)
  74. await expect(
  75. this.DocManager.promises.getFullDoc(this.project_id, this.doc_id)
  76. ).to.be.rejectedWith(this.stubbedError)
  77. })
  78. })
  79. describe('getRawDoc', function () {
  80. beforeEach(function () {
  81. this.DocManager.promises._getDoc = sinon.stub()
  82. this.doc = { lines: ['2134'] }
  83. })
  84. it('should call get doc with a quick filter', async function () {
  85. this.DocManager.promises._getDoc.resolves(this.doc)
  86. const doc = await this.DocManager.promises.getDocLines(
  87. this.project_id,
  88. this.doc_id
  89. )
  90. doc.should.equal(this.doc)
  91. this.DocManager.promises._getDoc
  92. .calledWith(this.project_id, this.doc_id, {
  93. lines: true,
  94. inS3: true,
  95. })
  96. .should.equal(true)
  97. })
  98. it('should return error when get doc errors', async function () {
  99. this.DocManager.promises._getDoc.rejects(this.stubbedError)
  100. await expect(
  101. this.DocManager.promises.getDocLines(this.project_id, this.doc_id)
  102. ).to.be.rejectedWith(this.stubbedError)
  103. })
  104. })
  105. describe('getDoc', function () {
  106. beforeEach(function () {
  107. this.project = { name: 'mock-project' }
  108. this.doc = {
  109. _id: this.doc_id,
  110. project_id: this.project_id,
  111. lines: ['mock-lines'],
  112. version: this.version,
  113. }
  114. })
  115. describe('when using a filter', function () {
  116. beforeEach(function () {
  117. this.MongoManager.promises.findDoc.resolves(this.doc)
  118. })
  119. it('should error if inS3 is not set to true', async function () {
  120. await expect(
  121. this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  122. inS3: false,
  123. })
  124. ).to.be.rejected
  125. })
  126. it('should always get inS3 even when no filter is passed', async function () {
  127. await expect(
  128. this.DocManager.promises._getDoc(this.project_id, this.doc_id)
  129. ).to.be.rejected
  130. this.MongoManager.promises.findDoc.called.should.equal(false)
  131. })
  132. it('should not error if inS3 is set to true', async function () {
  133. await this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  134. inS3: true,
  135. })
  136. })
  137. })
  138. describe('when the doc is in the doc collection', function () {
  139. beforeEach(async function () {
  140. this.MongoManager.promises.findDoc.resolves(this.doc)
  141. this.result = await this.DocManager.promises._getDoc(
  142. this.project_id,
  143. this.doc_id,
  144. { version: true, inS3: true }
  145. )
  146. })
  147. it('should get the doc from the doc collection', function () {
  148. this.MongoManager.promises.findDoc
  149. .calledWith(this.project_id, this.doc_id)
  150. .should.equal(true)
  151. })
  152. it('should return the doc with the version', function () {
  153. this.result.lines.should.equal(this.doc.lines)
  154. this.result.version.should.equal(this.version)
  155. })
  156. })
  157. describe('when MongoManager.findDoc errors', function () {
  158. it('should return the error', async function () {
  159. this.MongoManager.promises.findDoc.rejects(this.stubbedError)
  160. await expect(
  161. this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  162. version: true,
  163. inS3: true,
  164. })
  165. ).to.be.rejectedWith(this.stubbedError)
  166. })
  167. })
  168. describe('when the doc is archived', function () {
  169. beforeEach(async function () {
  170. this.doc = {
  171. _id: this.doc_id,
  172. project_id: this.project_id,
  173. version: 2,
  174. inS3: true,
  175. }
  176. this.unarchivedDoc = {
  177. _id: this.doc_id,
  178. project_id: this.project_id,
  179. lines: ['mock-lines'],
  180. version: 2,
  181. inS3: false,
  182. }
  183. this.MongoManager.promises.findDoc.resolves(this.doc)
  184. this.DocArchiveManager.promises.unarchiveDoc.callsFake(
  185. async (projectId, docId) => {
  186. this.MongoManager.promises.findDoc.resolves({
  187. ...this.unarchivedDoc,
  188. })
  189. }
  190. )
  191. this.result = await this.DocManager.promises._getDoc(
  192. this.project_id,
  193. this.doc_id,
  194. {
  195. version: true,
  196. inS3: true,
  197. }
  198. )
  199. })
  200. it('should call the DocArchive to unarchive the doc', function () {
  201. this.DocArchiveManager.promises.unarchiveDoc
  202. .calledWith(this.project_id, this.doc_id)
  203. .should.equal(true)
  204. })
  205. it('should look up the doc twice', function () {
  206. this.MongoManager.promises.findDoc.calledTwice.should.equal(true)
  207. })
  208. it('should return the doc', function () {
  209. expect(this.result).to.deep.equal({
  210. ...this.unarchivedDoc,
  211. })
  212. })
  213. })
  214. describe('when the doc does not exist in the docs collection', function () {
  215. it('should return a NotFoundError', async function () {
  216. this.MongoManager.promises.findDoc.resolves(null)
  217. await expect(
  218. this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  219. version: true,
  220. inS3: true,
  221. })
  222. ).to.be.rejectedWith(
  223. `No such doc: ${this.doc_id} in project ${this.project_id}`
  224. )
  225. })
  226. })
  227. })
  228. describe('getAllNonDeletedDocs', function () {
  229. describe('when the project exists', function () {
  230. beforeEach(async function () {
  231. this.docs = [
  232. {
  233. _id: this.doc_id,
  234. project_id: this.project_id,
  235. lines: ['mock-lines'],
  236. },
  237. ]
  238. this.MongoManager.promises.getProjectsDocs.resolves(this.docs)
  239. this.DocArchiveManager.promises.unArchiveAllDocs.resolves(this.docs)
  240. this.filter = { lines: true }
  241. this.result = await this.DocManager.promises.getAllNonDeletedDocs(
  242. this.project_id,
  243. this.filter
  244. )
  245. })
  246. it('should get the project from the database', function () {
  247. this.MongoManager.promises.getProjectsDocs.should.have.been.calledWith(
  248. this.project_id,
  249. { include_deleted: false },
  250. this.filter
  251. )
  252. })
  253. it('should return the docs', function () {
  254. expect(this.result).to.deep.equal(this.docs)
  255. })
  256. })
  257. describe('when there are no docs for the project', function () {
  258. it('should return a NotFoundError', async function () {
  259. this.MongoManager.promises.getProjectsDocs.resolves(null)
  260. this.DocArchiveManager.promises.unArchiveAllDocs.resolves(null)
  261. await expect(
  262. this.DocManager.promises.getAllNonDeletedDocs(
  263. this.project_id,
  264. this.filter
  265. )
  266. ).to.be.rejectedWith(`No docs for project ${this.project_id}`)
  267. })
  268. })
  269. })
  270. describe('patchDoc', function () {
  271. describe('when the doc exists', function () {
  272. beforeEach(function () {
  273. this.lines = ['mock', 'doc', 'lines']
  274. this.rev = 77
  275. this.MongoManager.promises.findDoc.resolves({
  276. _id: new ObjectId(this.doc_id),
  277. })
  278. this.meta = {}
  279. })
  280. describe('standard path', function () {
  281. beforeEach(async function () {
  282. await this.DocManager.promises.patchDoc(
  283. this.project_id,
  284. this.doc_id,
  285. this.meta
  286. )
  287. })
  288. it('should get the doc', function () {
  289. expect(this.MongoManager.promises.findDoc).to.have.been.calledWith(
  290. this.project_id,
  291. this.doc_id
  292. )
  293. })
  294. it('should persist the meta', function () {
  295. expect(this.MongoManager.promises.patchDoc).to.have.been.calledWith(
  296. this.project_id,
  297. this.doc_id,
  298. this.meta
  299. )
  300. })
  301. })
  302. describe('background flush disabled and deleting a doc', function () {
  303. beforeEach(async function () {
  304. this.settings.docstore.archiveOnSoftDelete = false
  305. this.meta.deleted = true
  306. await this.DocManager.promises.patchDoc(
  307. this.project_id,
  308. this.doc_id,
  309. this.meta
  310. )
  311. })
  312. it('should not flush the doc out of mongo', function () {
  313. expect(this.DocArchiveManager.promises.archiveDoc).to.not.have.been
  314. .called
  315. })
  316. })
  317. describe('background flush enabled and not deleting a doc', function () {
  318. beforeEach(async function () {
  319. this.settings.docstore.archiveOnSoftDelete = false
  320. this.meta.deleted = false
  321. await this.DocManager.promises.patchDoc(
  322. this.project_id,
  323. this.doc_id,
  324. this.meta
  325. )
  326. })
  327. it('should not flush the doc out of mongo', function () {
  328. expect(this.DocArchiveManager.promises.archiveDoc).to.not.have.been
  329. .called
  330. })
  331. })
  332. describe('background flush enabled and deleting a doc', function () {
  333. beforeEach(function () {
  334. this.settings.docstore.archiveOnSoftDelete = true
  335. this.meta.deleted = true
  336. })
  337. describe('when the background flush succeeds', function () {
  338. beforeEach(async function () {
  339. await this.DocManager.promises.patchDoc(
  340. this.project_id,
  341. this.doc_id,
  342. this.meta
  343. )
  344. })
  345. it('should not log a warning', function () {
  346. expect(this.logger.warn).to.not.have.been.called
  347. })
  348. it('should flush the doc out of mongo', function () {
  349. expect(
  350. this.DocArchiveManager.promises.archiveDoc
  351. ).to.have.been.calledWith(this.project_id, this.doc_id)
  352. })
  353. })
  354. describe('when the background flush fails', function () {
  355. beforeEach(async function () {
  356. this.err = new Error('foo')
  357. this.DocArchiveManager.promises.archiveDoc.rejects(this.err)
  358. await this.DocManager.promises.patchDoc(
  359. this.project_id,
  360. this.doc_id,
  361. this.meta
  362. )
  363. })
  364. it('should log a warning', function () {
  365. expect(this.logger.warn).to.have.been.calledWith(
  366. sinon.match({
  367. projectId: this.project_id,
  368. docId: this.doc_id,
  369. err: this.err,
  370. }),
  371. 'archiving a single doc in the background failed'
  372. )
  373. })
  374. })
  375. })
  376. })
  377. describe('when the doc does not exist', function () {
  378. it('should return a NotFoundError', async function () {
  379. this.MongoManager.promises.findDoc.resolves(null)
  380. await expect(
  381. this.DocManager.promises.patchDoc(this.project_id, this.doc_id, {})
  382. ).to.be.rejectedWith(
  383. `No such project/doc to delete: ${this.project_id}/${this.doc_id}`
  384. )
  385. })
  386. })
  387. })
  388. describe('updateDoc', function () {
  389. beforeEach(function () {
  390. this.oldDocLines = ['old', 'doc', 'lines']
  391. this.newDocLines = ['new', 'doc', 'lines']
  392. this.originalRanges = {
  393. changes: [
  394. {
  395. id: new ObjectId().toString(),
  396. op: { i: 'foo', p: 3 },
  397. meta: {
  398. user_id: new ObjectId().toString(),
  399. ts: new Date().toString(),
  400. },
  401. },
  402. ],
  403. }
  404. this.newRanges = {
  405. changes: [
  406. {
  407. id: new ObjectId().toString(),
  408. op: { i: 'bar', p: 6 },
  409. meta: {
  410. user_id: new ObjectId().toString(),
  411. ts: new Date().toString(),
  412. },
  413. },
  414. ],
  415. }
  416. this.version = 42
  417. this.doc = {
  418. _id: this.doc_id,
  419. project_id: this.project_id,
  420. lines: this.oldDocLines,
  421. rev: (this.rev = 5),
  422. version: this.version,
  423. ranges: this.originalRanges,
  424. }
  425. this.DocManager.promises._getDoc = sinon.stub()
  426. })
  427. describe('when only the doc lines have changed', function () {
  428. beforeEach(async function () {
  429. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  430. this.result = await this.DocManager.promises.updateDoc(
  431. this.project_id,
  432. this.doc_id,
  433. this.newDocLines,
  434. this.version,
  435. this.originalRanges
  436. )
  437. })
  438. it('should get the existing doc', function () {
  439. this.DocManager.promises._getDoc
  440. .calledWith(this.project_id, this.doc_id, {
  441. version: true,
  442. rev: true,
  443. lines: true,
  444. ranges: true,
  445. inS3: true,
  446. })
  447. .should.equal(true)
  448. })
  449. it('should upsert the document to the doc collection', function () {
  450. this.MongoManager.promises.upsertIntoDocCollection
  451. .calledWith(this.project_id, this.doc_id, this.rev, {
  452. lines: this.newDocLines,
  453. })
  454. .should.equal(true)
  455. })
  456. it('should return the new rev', function () {
  457. expect(this.result).to.deep.equal({ modified: true, rev: this.rev + 1 })
  458. })
  459. })
  460. describe('when the doc ranges have changed', function () {
  461. beforeEach(async function () {
  462. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  463. this.RangeManager.shouldUpdateRanges.returns(true)
  464. this.result = await this.DocManager.promises.updateDoc(
  465. this.project_id,
  466. this.doc_id,
  467. this.oldDocLines,
  468. this.version,
  469. this.newRanges
  470. )
  471. })
  472. it('should upsert the ranges', function () {
  473. this.MongoManager.promises.upsertIntoDocCollection
  474. .calledWith(this.project_id, this.doc_id, this.rev, {
  475. ranges: this.newRanges,
  476. })
  477. .should.equal(true)
  478. })
  479. it('should return the new rev', function () {
  480. expect(this.result).to.deep.equal({ modified: true, rev: this.rev + 1 })
  481. })
  482. })
  483. describe('when only the version has changed', function () {
  484. beforeEach(async function () {
  485. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  486. this.result = await this.DocManager.promises.updateDoc(
  487. this.project_id,
  488. this.doc_id,
  489. this.oldDocLines,
  490. this.version + 1,
  491. this.originalRanges
  492. )
  493. })
  494. it('should update the version', function () {
  495. this.MongoManager.promises.upsertIntoDocCollection.should.have.been.calledWith(
  496. this.project_id,
  497. this.doc_id,
  498. this.rev,
  499. { version: this.version + 1 }
  500. )
  501. })
  502. it('should return the old rev', function () {
  503. expect(this.result).to.deep.equal({ modified: true, rev: this.rev })
  504. })
  505. })
  506. describe('when the doc has not changed at all', function () {
  507. beforeEach(async function () {
  508. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  509. this.result = await this.DocManager.promises.updateDoc(
  510. this.project_id,
  511. this.doc_id,
  512. this.oldDocLines,
  513. this.version,
  514. this.originalRanges
  515. )
  516. })
  517. it('should not update the ranges or lines or version', function () {
  518. this.MongoManager.promises.upsertIntoDocCollection.called.should.equal(
  519. false
  520. )
  521. })
  522. it('should return the old rev and modified == false', function () {
  523. expect(this.result).to.deep.equal({ modified: false, rev: this.rev })
  524. })
  525. })
  526. describe('when the version is null', function () {
  527. it('should return an error', async function () {
  528. await expect(
  529. this.DocManager.promises.updateDoc(
  530. this.project_id,
  531. this.doc_id,
  532. this.newDocLines,
  533. null,
  534. this.originalRanges
  535. )
  536. ).to.be.rejectedWith('no lines, version or ranges provided')
  537. })
  538. })
  539. describe('when the lines are null', function () {
  540. it('should return an error', async function () {
  541. await expect(
  542. this.DocManager.promises.updateDoc(
  543. this.project_id,
  544. this.doc_id,
  545. null,
  546. this.version,
  547. this.originalRanges
  548. )
  549. ).to.be.rejectedWith('no lines, version or ranges provided')
  550. })
  551. })
  552. describe('when the ranges are null', function () {
  553. it('should return an error', async function () {
  554. await expect(
  555. this.DocManager.promises.updateDoc(
  556. this.project_id,
  557. this.doc_id,
  558. this.newDocLines,
  559. this.version,
  560. null
  561. )
  562. ).to.be.rejectedWith('no lines, version or ranges provided')
  563. })
  564. })
  565. describe('when there is a generic error getting the doc', function () {
  566. beforeEach(async function () {
  567. this.error = new Error('doc could not be found')
  568. this.DocManager.promises._getDoc = sinon.stub().rejects(this.error)
  569. await expect(
  570. this.DocManager.promises.updateDoc(
  571. this.project_id,
  572. this.doc_id,
  573. this.newDocLines,
  574. this.version,
  575. this.originalRanges
  576. )
  577. ).to.be.rejectedWith(this.error)
  578. })
  579. it('should not upsert the document to the doc collection', function () {
  580. this.MongoManager.promises.upsertIntoDocCollection.should.not.have.been
  581. .called
  582. })
  583. })
  584. describe('when the version was decremented', function () {
  585. it('should return an error', async function () {
  586. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  587. await expect(
  588. this.DocManager.promises.updateDoc(
  589. this.project_id,
  590. this.doc_id,
  591. this.newDocLines,
  592. this.version - 1,
  593. this.originalRanges
  594. )
  595. ).to.be.rejectedWith(Errors.DocVersionDecrementedError)
  596. })
  597. })
  598. describe('when the doc lines have not changed', function () {
  599. beforeEach(async function () {
  600. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  601. this.result = await this.DocManager.promises.updateDoc(
  602. this.project_id,
  603. this.doc_id,
  604. this.oldDocLines.slice(),
  605. this.version,
  606. this.originalRanges
  607. )
  608. })
  609. it('should not update the doc', function () {
  610. this.MongoManager.promises.upsertIntoDocCollection.called.should.equal(
  611. false
  612. )
  613. })
  614. it('should return the existing rev', function () {
  615. expect(this.result).to.deep.equal({ modified: false, rev: this.rev })
  616. })
  617. })
  618. describe('when the doc does not exist', function () {
  619. beforeEach(async function () {
  620. this.DocManager.promises._getDoc = sinon.stub().resolves(null)
  621. this.result = await this.DocManager.promises.updateDoc(
  622. this.project_id,
  623. this.doc_id,
  624. this.newDocLines,
  625. this.version,
  626. this.originalRanges
  627. )
  628. })
  629. it('should upsert the document to the doc collection', function () {
  630. this.MongoManager.promises.upsertIntoDocCollection.should.have.been.calledWith(
  631. this.project_id,
  632. this.doc_id,
  633. undefined,
  634. {
  635. lines: this.newDocLines,
  636. ranges: this.originalRanges,
  637. version: this.version,
  638. }
  639. )
  640. })
  641. it('should return the new rev', function () {
  642. expect(this.result).to.deep.equal({ modified: true, rev: 1 })
  643. })
  644. })
  645. describe('when another update is racing', function () {
  646. beforeEach(async function () {
  647. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  648. this.MongoManager.promises.upsertIntoDocCollection
  649. .onFirstCall()
  650. .rejects(new Errors.DocRevValueError())
  651. this.RangeManager.shouldUpdateRanges.returns(true)
  652. this.result = await this.DocManager.promises.updateDoc(
  653. this.project_id,
  654. this.doc_id,
  655. this.newDocLines,
  656. this.version + 1,
  657. this.newRanges
  658. )
  659. })
  660. it('should upsert the doc twice', function () {
  661. this.MongoManager.promises.upsertIntoDocCollection.should.have.been.calledWith(
  662. this.project_id,
  663. this.doc_id,
  664. this.rev,
  665. {
  666. ranges: this.newRanges,
  667. lines: this.newDocLines,
  668. version: this.version + 1,
  669. }
  670. )
  671. this.MongoManager.promises.upsertIntoDocCollection.should.have.been
  672. .calledTwice
  673. })
  674. it('should return the new rev', function () {
  675. expect(this.result).to.deep.equal({ modified: true, rev: this.rev + 1 })
  676. })
  677. })
  678. })
  679. })