Przeglądaj źródła

fix: sync between primary and secondary database with diff

Karol Sójko 1 rok temu
rodzic
commit
fab5d18064

+ 21 - 1
packages/revisions/src/Domain/UseCase/Transition/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser.ts

@@ -39,12 +39,19 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
     if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
       const newRevisions = await this.getNewRevisionsCreatedInSecondaryDatabase(userUuid)
       for (const existingRevision of newRevisions.alreadyExistingInPrimary) {
+        this.logger.info(`Removing revision ${existingRevision.id.toString()} from secondary database`)
         await (this.secondRevisionsRepository as RevisionRepositoryInterface).removeOneByUuid(
           Uuid.create(existingRevision.id.toString()).getValue(),
           userUuid,
         )
       }
 
+      if (newRevisions.newRevisionsInSecondary.length > 0) {
+        this.logger.info(
+          `Found ${newRevisions.newRevisionsInSecondary.length} new revisions in secondary database for user ${userUuid.value}`,
+        )
+      }
+
       newRevisionsInSecondaryCount = newRevisions.newRevisionsInSecondary.length
     }
 
@@ -170,7 +177,14 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
       this.secondRevisionsRepository as RevisionRepositoryInterface
     ).countByUserUuid(userUuid)
 
-    return totalRevisionsCountForUserInSecondary > 0
+    const hasAlreadyDataInSecondaryDatabase = totalRevisionsCountForUserInSecondary > 0
+    if (hasAlreadyDataInSecondaryDatabase) {
+      this.logger.info(
+        `User ${userUuid.value} has already ${totalRevisionsCountForUserInSecondary} revisions in secondary database`,
+      )
+    }
+
+    return hasAlreadyDataInSecondaryDatabase
   }
 
   private async getNewRevisionsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
@@ -226,6 +240,12 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
   private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> {
     const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
 
+    if (totalRevisionsCountForUserInPrimary > 0) {
+      this.logger.info(
+        `User ${userUuid.value} has ${totalRevisionsCountForUserInPrimary} revisions in primary database.`,
+      )
+    }
+
     return totalRevisionsCountForUserInPrimary === 0
   }
 

+ 0 - 417
packages/syncing-server/src/Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser.spec.ts

@@ -1,417 +0,0 @@
-import { Logger } from 'winston'
-
-import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
-import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from './TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
-import { Item } from '../../../Item/Item'
-import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
-import { TimerInterface } from '@standardnotes/time'
-
-describe('TransitionItemsFromPrimaryToSecondaryDatabaseForUser', () => {
-  let primaryItemRepository: ItemRepositoryInterface
-  let secondaryItemRepository: ItemRepositoryInterface | null
-  let logger: Logger
-  let primaryItem1: Item
-  let primaryItem2: Item
-  let secondaryItem1: Item
-  let secondaryItem2: Item
-  let timer: TimerInterface
-
-  const createUseCase = () =>
-    new TransitionItemsFromPrimaryToSecondaryDatabaseForUser(
-      primaryItemRepository,
-      secondaryItemRepository,
-      timer,
-      logger,
-      1,
-    )
-
-  beforeEach(() => {
-    primaryItem1 = Item.create(
-      {
-        duplicateOf: null,
-        itemsKeyId: 'items-key-id=1',
-        content: 'content-1',
-        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
-        encItemKey: 'enc-item-key-1',
-        authHash: 'auth-hash-1',
-        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
-        deleted: false,
-        updatedWithSession: null,
-        dates: Dates.create(new Date(123), new Date(123)).getValue(),
-        timestamps: Timestamps.create(123, 123).getValue(),
-      },
-      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
-    ).getValue()
-
-    primaryItem2 = Item.create(
-      {
-        duplicateOf: null,
-        itemsKeyId: 'items-key-id=2',
-        content: 'content-2',
-        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
-        encItemKey: 'enc-item-key-2',
-        authHash: 'auth-hash-2',
-        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
-        deleted: true,
-        updatedWithSession: null,
-        dates: Dates.create(new Date(123), new Date(123)).getValue(),
-        timestamps: Timestamps.create(123, 123).getValue(),
-      },
-      new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
-    ).getValue()
-
-    secondaryItem1 = Item.create(
-      {
-        duplicateOf: null,
-        itemsKeyId: 'items-key-id=1',
-        content: 'content-1',
-        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
-        encItemKey: 'enc-item-key-1',
-        authHash: 'auth-hash-1',
-        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
-        deleted: false,
-        updatedWithSession: null,
-        dates: Dates.create(new Date(123), new Date(123)).getValue(),
-        timestamps: Timestamps.create(123, 123).getValue(),
-      },
-      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
-    ).getValue()
-
-    secondaryItem2 = Item.create(
-      {
-        duplicateOf: null,
-        itemsKeyId: 'items-key-id=2',
-        content: 'content-2',
-        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
-        encItemKey: 'enc-item-key-2',
-        authHash: 'auth-hash-2',
-        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
-        deleted: true,
-        updatedWithSession: null,
-        dates: Dates.create(new Date(123), new Date(123)).getValue(),
-        timestamps: Timestamps.create(123, 123).getValue(),
-      },
-      new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
-    ).getValue()
-
-    primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    primaryItemRepository.countAll = jest.fn().mockResolvedValue(2)
-    primaryItemRepository.findAll = jest
-      .fn()
-      .mockResolvedValueOnce([primaryItem1])
-      .mockResolvedValueOnce([primaryItem2])
-      .mockResolvedValueOnce([primaryItem1])
-      .mockResolvedValueOnce([primaryItem2])
-    primaryItemRepository.deleteByUserUuid = jest.fn().mockResolvedValue(undefined)
-
-    secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    secondaryItemRepository.save = jest.fn().mockResolvedValue(undefined)
-    secondaryItemRepository.deleteByUserUuid = jest.fn().mockResolvedValue(undefined)
-    secondaryItemRepository.countAll = jest.fn().mockReturnValueOnce(0).mockReturnValueOnce(2)
-    secondaryItemRepository.findByUuid = jest
-      .fn()
-      .mockResolvedValueOnce(secondaryItem1)
-      .mockResolvedValueOnce(secondaryItem2)
-
-    logger = {} as jest.Mocked<Logger>
-    logger.error = jest.fn()
-    logger.info = jest.fn()
-
-    timer = {} as jest.Mocked<TimerInterface>
-    timer.sleep = jest.fn()
-    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
-    timer.convertMicrosecondsToTimeStructure = jest.fn().mockReturnValue({
-      days: 0,
-      hours: 0,
-      minutes: 0,
-      seconds: 0,
-      milliseconds: 0,
-    })
-  })
-
-  describe('successfull transition', () => {
-    it('should transition items from primary to secondary database', async () => {
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeFalsy()
-
-      expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(3)
-      expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
-      expect(primaryItemRepository.findAll).toHaveBeenCalledTimes(4)
-      expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(1, {
-        userUuid: '00000000-0000-0000-0000-000000000000',
-        limit: 1,
-        offset: 0,
-      })
-      expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(2, {
-        userUuid: '00000000-0000-0000-0000-000000000000',
-        limit: 1,
-        offset: 1,
-      })
-      expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(3, {
-        userUuid: '00000000-0000-0000-0000-000000000000',
-        limit: 1,
-        offset: 0,
-      })
-      expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(4, {
-        userUuid: '00000000-0000-0000-0000-000000000000',
-        limit: 1,
-        offset: 1,
-      })
-      expect((secondaryItemRepository as ItemRepositoryInterface).save).toHaveBeenCalledTimes(2)
-      expect((secondaryItemRepository as ItemRepositoryInterface).save).toHaveBeenCalledWith(primaryItem1)
-      expect((secondaryItemRepository as ItemRepositoryInterface).save).toHaveBeenCalledWith(primaryItem2)
-      expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).not.toHaveBeenCalled()
-      expect(primaryItemRepository.deleteByUserUuid).toHaveBeenCalledTimes(1)
-    })
-
-    it('should log an error if deleting items from primary database fails', async () => {
-      primaryItemRepository.deleteByUserUuid = jest.fn().mockRejectedValue(new Error('error'))
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeFalsy()
-
-      expect(logger.error).toHaveBeenCalledTimes(1)
-      expect(logger.error).toHaveBeenCalledWith(
-        'Failed to clean up primary database items for user 00000000-0000-0000-0000-000000000000: error',
-      )
-    })
-  })
-
-  describe('failed transition', () => {
-    it('should remove items from secondary database if integrity check fails', async () => {
-      const secondaryItem2WithDifferentContent = Item.create({
-        ...secondaryItem2.props,
-        content: 'different-content',
-      }).getValue()
-
-      ;(secondaryItemRepository as ItemRepositoryInterface).findByUuid = jest
-        .fn()
-        .mockResolvedValueOnce(secondaryItem1)
-        .mockResolvedValueOnce(secondaryItem2WithDifferentContent)
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-
-      expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
-      expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-    })
-
-    it('should remove items from secondary database if migrating items fails', async () => {
-      primaryItemRepository.findAll = jest
-        .fn()
-        .mockResolvedValueOnce([primaryItem1])
-        .mockRejectedValueOnce(new Error('error'))
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-      expect(result.getError()).toEqual('error')
-
-      expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
-      expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-    })
-
-    it('should log an error if deleting items from secondary database fails upon migration failure', async () => {
-      primaryItemRepository.findAll = jest
-        .fn()
-        .mockResolvedValueOnce([primaryItem1])
-        .mockRejectedValueOnce(new Error('error'))
-      ;(secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid = jest
-        .fn()
-        .mockRejectedValue(new Error('error'))
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-
-      expect(logger.error).toHaveBeenCalledTimes(1)
-      expect(logger.error).toHaveBeenCalledWith(
-        'Failed to clean up secondary database items for user 00000000-0000-0000-0000-000000000000: error',
-      )
-    })
-
-    it('should log an error if deleting items from secondary database fails upon integrity check failure', async () => {
-      const secondaryItem2WithDifferentContent = Item.create({
-        ...secondaryItem2.props,
-        content: 'different-content',
-      }).getValue()
-
-      ;(secondaryItemRepository as ItemRepositoryInterface).findByUuid = jest
-        .fn()
-        .mockResolvedValueOnce(secondaryItem1)
-        .mockResolvedValueOnce(secondaryItem2WithDifferentContent)
-      ;(secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid = jest
-        .fn()
-        .mockRejectedValue(new Error('error'))
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-
-      expect(logger.error).toHaveBeenCalledTimes(1)
-      expect(logger.error).toHaveBeenCalledWith(
-        'Failed to clean up secondary database items for user 00000000-0000-0000-0000-000000000000: error',
-      )
-    })
-
-    it('should not perform the transition if secondary item repository is not set', async () => {
-      secondaryItemRepository = null
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-      expect(result.getError()).toEqual('Secondary item repository is not set')
-
-      expect(primaryItemRepository.countAll).not.toHaveBeenCalled()
-      expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
-      expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-    })
-
-    it('should not perform the transition if the user uuid is invalid', async () => {
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: 'invalid-uuid',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-      expect(result.getError()).toEqual('Given value is not a valid uuid: invalid-uuid')
-
-      expect(primaryItemRepository.countAll).not.toHaveBeenCalled()
-      expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
-      expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-    })
-
-    it('should fail integrity check if the item count is not the same in both databases', async () => {
-      ;(secondaryItemRepository as ItemRepositoryInterface).countAll = jest
-        .fn()
-        .mockResolvedValueOnce(0)
-        .mockResolvedValueOnce(1)
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-      expect(result.getError()).toEqual(
-        'Total items count for user 00000000-0000-0000-0000-000000000000 in primary database (2) does not match total items count in secondary database (1)',
-      )
-
-      expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(3)
-      expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
-      expect((secondaryItemRepository as ItemRepositoryInterface).countAll).toHaveBeenCalledTimes(2)
-      expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-      expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
-    })
-
-    it('should fail if one item is not found in the secondary database', async () => {
-      ;(secondaryItemRepository as ItemRepositoryInterface).findByUuid = jest
-        .fn()
-        .mockResolvedValueOnce(secondaryItem1)
-        .mockResolvedValueOnce(null)
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-      expect(result.getError()).toEqual('Item 00000000-0000-0000-0000-000000000001 not found in secondary database')
-
-      expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(3)
-      expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
-      expect((secondaryItemRepository as ItemRepositoryInterface).countAll).toHaveBeenCalledTimes(2)
-      expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-      expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
-    })
-
-    it('should fail if an error is thrown during integrity check between primary and secondary database', async () => {
-      ;(secondaryItemRepository as ItemRepositoryInterface).countAll = jest
-        .fn()
-        .mockReturnValueOnce(0)
-        .mockRejectedValue(new Error('error'))
-
-      const useCase = createUseCase()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-      expect(result.getError()).toEqual('error')
-
-      expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-      expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
-    })
-  })
-
-  it('should not migrate items if there are no items in the primary database', async () => {
-    primaryItemRepository.countAll = jest.fn().mockResolvedValue(0)
-
-    const useCase = createUseCase()
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-    })
-
-    expect(result.isFailed()).toBeFalsy()
-
-    expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(1)
-    expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
-    expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
-    expect((secondaryItemRepository as ItemRepositoryInterface).save).not.toHaveBeenCalled()
-    expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-    expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).not.toHaveBeenCalled()
-  })
-
-  it('should not migrate items if there are items in the secondary database', async () => {
-    ;(secondaryItemRepository as ItemRepositoryInterface).countAll = jest.fn().mockResolvedValue(1)
-
-    const useCase = createUseCase()
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-    })
-
-    expect(result.isFailed()).toBeTruthy()
-
-    expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
-    expect((secondaryItemRepository as ItemRepositoryInterface).save).not.toHaveBeenCalled()
-    expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
-    expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).not.toHaveBeenCalled()
-  })
-})

+ 82 - 6
packages/syncing-server/src/Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser.ts

@@ -1,3 +1,4 @@
+/* istanbul ignore file */
 import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 import { Logger } from 'winston'
 
@@ -5,6 +6,7 @@ import { TransitionItemsFromPrimaryToSecondaryDatabaseForUserDTO } from './Trans
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { ItemQuery } from '../../../Item/ItemQuery'
 import { TimerInterface } from '@standardnotes/time'
+import { Item } from '../../../Item/Item'
 
 export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
   constructor(
@@ -34,8 +36,21 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
       return Result.ok()
     }
 
+    let newItemsInSecondaryCount = 0
     if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
-      return Result.fail(`Items for user ${userUuid.value} already exist in secondary database`)
+      const newItems = await this.getNewItemsCreatedInSecondaryDatabase(userUuid)
+      for (const existingItem of newItems.alreadyExistingInPrimary) {
+        this.logger.info(`Removing item ${existingItem.uuid.value} from secondary database`)
+        await (this.secondaryItemRepository as ItemRepositoryInterface).remove(existingItem)
+      }
+
+      if (newItems.newItemsInSecondary.length > 0) {
+        this.logger.info(
+          `Found ${newItems.newItemsInSecondary.length} new items in secondary database for user ${userUuid.value}`,
+        )
+      }
+
+      newItemsInSecondaryCount = newItems.newItemsInSecondary.length
     }
 
     const migrationTimeStart = this.timer.getTimestampInMicroseconds()
@@ -54,7 +69,10 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
 
     await this.allowForSecondaryDatabaseToCatchUp()
 
-    const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid)
+    const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
+      userUuid,
+      newItemsInSecondaryCount,
+    )
     if (integrityCheckResult.isFailed()) {
       const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
       if (cleanupResult.isFailed()) {
@@ -90,12 +108,21 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
       userUuid: userUuid.value,
     })
 
-    return totalItemsCountForUser > 0
+    const hasAlreadyDataInSecondaryDatabase = totalItemsCountForUser > 0
+    if (hasAlreadyDataInSecondaryDatabase) {
+      this.logger.info(`User ${userUuid.value} has already ${totalItemsCountForUser} items in secondary database`)
+    }
+
+    return hasAlreadyDataInSecondaryDatabase
   }
 
   private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> {
     const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
 
+    if (totalItemsCountForUser > 0) {
+      this.logger.info(`User ${userUuid.value} has ${totalItemsCountForUser} items in primary database.`)
+    }
+
     return totalItemsCountForUser === 0
   }
 
@@ -104,6 +131,52 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
     await this.timer.sleep(twoSecondsInMilliseconds)
   }
 
+  private async getNewItemsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
+    alreadyExistingInPrimary: Item[]
+    newItemsInSecondary: Item[]
+  }> {
+    const items = await (this.secondaryItemRepository as ItemRepositoryInterface).findAll({
+      userUuid: userUuid.value,
+    })
+
+    const alreadyExistingInPrimary: Item[] = []
+    const newItemsInSecondary: Item[] = []
+
+    for (const item of items) {
+      const itemExistsInPrimary = await this.checkIfItemExistsInPrimaryDatabase(item)
+      if (itemExistsInPrimary) {
+        alreadyExistingInPrimary.push(item)
+      } else {
+        newItemsInSecondary.push(item)
+      }
+    }
+
+    return {
+      alreadyExistingInPrimary: alreadyExistingInPrimary,
+      newItemsInSecondary: newItemsInSecondary,
+    }
+  }
+
+  private async checkIfItemExistsInPrimaryDatabase(item: Item): Promise<boolean> {
+    const itemInPrimary = await this.primaryItemRepository.findByUuid(item.uuid)
+
+    if (itemInPrimary === null) {
+      return false
+    }
+
+    if (!item.isIdenticalTo(itemInPrimary)) {
+      this.logger.error(
+        `Revision ${item.id.toString()} is not identical in primary and secondary database. Revision in secondary database: ${JSON.stringify(
+          item,
+        )}, revision in primary database: ${JSON.stringify(itemInPrimary)}`,
+      )
+
+      return false
+    }
+
+    return true
+  }
+
   private async migrateItemsForUser(userUuid: Uuid): Promise<Result<void>> {
     try {
       const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
@@ -138,7 +211,10 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
     }
   }
 
-  private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid: Uuid): Promise<Result<boolean>> {
+  private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
+    userUuid: Uuid,
+    newItemsInSecondaryCount: number,
+  ): Promise<Result<boolean>> {
     try {
       const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
       const totalItemsCountForUserInSecondary = await (
@@ -147,9 +223,9 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
         userUuid: userUuid.value,
       })
 
-      if (totalItemsCountForUserInPrimary !== totalItemsCountForUserInSecondary) {
+      if (totalItemsCountForUserInPrimary + newItemsInSecondaryCount !== totalItemsCountForUserInSecondary) {
         return Result.fail(
-          `Total items count for user ${userUuid.value} in primary database (${totalItemsCountForUserInPrimary}) does not match total items count in secondary database (${totalItemsCountForUserInSecondary})`,
+          `Total items count for user ${userUuid.value} in primary database (${totalItemsCountForUserInPrimary} + ${newItemsInSecondaryCount}) does not match total items count in secondary database (${totalItemsCountForUserInSecondary})`,
         )
       }