浏览代码

feat: add skipping verified transitions (#827)

* fix(syncing-server): remove transitioning individual users

* feat: add skipping verified transitions

* fix(auth): remove unused use case
Karol Sójko 1 年之前
父节点
当前提交
d4d49454a6
共有 22 个文件被更改,包括 115 次插入370 次删除
  1. 29 1
      packages/auth/bin/transition.ts
  2. 0 9
      packages/auth/src/Bootstrap/Container.ts
  3. 0 1
      packages/auth/src/Bootstrap/Types.ts
  4. 2 6
      packages/auth/src/Domain/Transition/TransitionStatusRepositoryInterface.ts
  5. 0 114
      packages/auth/src/Domain/UseCase/GetTransitionStatus/GetTransitionStatus.spec.ts
  6. 0 43
      packages/auth/src/Domain/UseCase/GetTransitionStatus/GetTransitionStatus.ts
  7. 0 4
      packages/auth/src/Domain/UseCase/GetTransitionStatus/GetTransitionStatusDTO.ts
  8. 2 39
      packages/auth/src/Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus.spec.ts
  9. 2 8
      packages/auth/src/Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus.ts
  10. 1 1
      packages/auth/src/Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatusDTO.ts
  11. 0 18
      packages/auth/src/Infra/InMemory/InMemoryTransitionStatusRepository.ts
  12. 19 33
      packages/auth/src/Infra/Redis/RedisTransitionStatusRepository.ts
  13. 1 1
      packages/domain-events/src/Domain/Event/TransitionStatusUpdatedEventPayload.ts
  14. 2 2
      packages/revisions/src/Domain/Event/DomainEventFactory.ts
  15. 1 1
      packages/revisions/src/Domain/Event/DomainEventFactoryInterface.ts
  16. 40 15
      packages/revisions/src/Domain/Handler/TransitionStatusUpdatedEventHandler.ts
  17. 0 54
      packages/syncing-server/bin/transition.ts
  18. 0 11
      packages/syncing-server/docker/entrypoint-transition.js
  19. 0 6
      packages/syncing-server/docker/entrypoint.sh
  20. 1 1
      packages/syncing-server/src/Domain/Event/DomainEventFactory.ts
  21. 1 1
      packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts
  22. 14 1
      packages/syncing-server/src/Domain/Handler/TransitionStatusUpdatedEventHandler.ts

+ 29 - 1
packages/auth/bin/transition.ts

@@ -11,12 +11,15 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
 import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
 import { TimerInterface } from '@standardnotes/time'
+import { TransitionStatusRepositoryInterface } from '../src/Domain/Transition/TransitionStatusRepositoryInterface'
+import { RoleName } from '@standardnotes/domain-core'
 
 const inputArgs = process.argv.slice(2)
 const startDateString = inputArgs[0]
 const endDateString = inputArgs[1]
 
 const requestTransition = async (
+  transitionStatusRepository: TransitionStatusRepositoryInterface,
   userRepository: UserRepositoryInterface,
   logger: Logger,
   domainEventFactory: DomainEventFactoryInterface,
@@ -36,6 +39,19 @@ const requestTransition = async (
 
   let usersTriggered = 0
   for (const user of users) {
+    const itemsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'items')
+    const revisionsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'revisions')
+
+    const userRoles = await user.roles
+
+    const userHasTransitionRole = userRoles.some((role) => role.name === RoleName.NAMES.TransitionUser)
+    const bothTransitionStatusesAreVerified =
+      itemsTransitionStatus === 'VERIFIED' && revisionsTransitionStatus === 'VERIFIED'
+
+    if (userHasTransitionRole && bothTransitionStatusesAreVerified) {
+      continue
+    }
+
     const transitionRequestedEvent = domainEventFactory.createTransitionRequestedEvent({
       userUuid: user.uuid,
       type: 'items',
@@ -67,8 +83,20 @@ void container.load().then((container) => {
   const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
   const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
   const timer = container.get<TimerInterface>(TYPES.Auth_Timer)
+  const transitionStatusRepository = container.get<TransitionStatusRepositoryInterface>(
+    TYPES.Auth_TransitionStatusRepository,
+  )
 
-  Promise.resolve(requestTransition(userRepository, logger, domainEventFactory, domainEventPublisher, timer))
+  Promise.resolve(
+    requestTransition(
+      transitionStatusRepository,
+      userRepository,
+      logger,
+      domainEventFactory,
+      domainEventPublisher,
+      timer,
+    ),
+  )
     .then(() => {
       logger.info(`Finished transition request for users created between ${startDateString} and ${endDateString}`)
 

+ 0 - 9
packages/auth/src/Bootstrap/Container.ts

@@ -263,7 +263,6 @@ import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionS
 import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
 import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
 import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
-import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
 import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
 import { SharedVaultUserPersistenceMapper } from '../Mapping/SharedVaultUserPersistenceMapper'
 import { SharedVaultUserRepositoryInterface } from '../Domain/SharedVault/SharedVaultUserRepositoryInterface'
@@ -945,14 +944,6 @@ export class ContainerConfigLoader {
           container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
         ),
       )
-    container
-      .bind<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus)
-      .toConstantValue(
-        new GetTransitionStatus(
-          container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
-          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
-        ),
-      )
     container
       .bind<AddSharedVaultUser>(TYPES.Auth_AddSharedVaultUser)
       .toConstantValue(

+ 0 - 1
packages/auth/src/Bootstrap/Types.ts

@@ -159,7 +159,6 @@ const TYPES = {
   Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
   Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
   Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
-  Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
   Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
   Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'),
   // Handlers

+ 2 - 6
packages/auth/src/Domain/Transition/TransitionStatusRepositoryInterface.ts

@@ -2,14 +2,10 @@ export interface TransitionStatusRepositoryInterface {
   updateStatus(
     userUuid: string,
     transitionType: 'items' | 'revisions',
-    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED',
+    status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED',
   ): Promise<void>
-  removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void>
   getStatus(
     userUuid: string,
     transitionType: 'items' | 'revisions',
-  ): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null>
-  getStatuses(
-    transitionType: 'items' | 'revisions',
-  ): Promise<Array<{ userUuid: string; status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' }>>
+  ): Promise<'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null>
 }

+ 0 - 114
packages/auth/src/Domain/UseCase/GetTransitionStatus/GetTransitionStatus.spec.ts

@@ -1,114 +0,0 @@
-import { RoleName } from '@standardnotes/domain-core'
-import { Role } from '../../Role/Role'
-import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
-import { User } from '../../User/User'
-import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
-import { GetTransitionStatus } from './GetTransitionStatus'
-
-describe('GetTransitionStatus', () => {
-  let transitionStatusRepository: TransitionStatusRepositoryInterface
-  let userRepository: UserRepositoryInterface
-  let user: User
-  let role: Role
-
-  const createUseCase = () => new GetTransitionStatus(transitionStatusRepository, userRepository)
-
-  beforeEach(() => {
-    transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
-    transitionStatusRepository.getStatus = jest.fn().mockReturnValue(null)
-
-    role = {} as jest.Mocked<Role>
-    role.name = RoleName.NAMES.CoreUser
-
-    user = {
-      uuid: '00000000-0000-0000-0000-000000000000',
-      email: 'test@test.te',
-    } as jest.Mocked<User>
-    user.roles = Promise.resolve([role])
-
-    userRepository = {} as jest.Mocked<UserRepositoryInterface>
-    userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
-  })
-
-  it('returns transition status FINISHED', async () => {
-    role.name = RoleName.NAMES.TransitionUser
-    user.roles = Promise.resolve([role])
-    userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
-
-    const useCase = createUseCase()
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-      transitionType: 'items',
-    })
-
-    expect(result.isFailed()).toBeFalsy()
-    expect(result.getValue()).toEqual('FINISHED')
-  })
-
-  it('returns transition status STARTED', async () => {
-    const useCase = createUseCase()
-
-    transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-      transitionType: 'items',
-    })
-
-    expect(result.isFailed()).toBeFalsy()
-    expect(result.getValue()).toEqual('STARTED')
-  })
-
-  it('returns transition status TO-DO', async () => {
-    const useCase = createUseCase()
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-      transitionType: 'items',
-    })
-
-    expect(result.isFailed()).toBeFalsy()
-    expect(result.getValue()).toEqual('TO-DO')
-  })
-
-  it('returns transition status FAILED', async () => {
-    const useCase = createUseCase()
-
-    transitionStatusRepository.getStatus = jest.fn().mockReturnValue('FAILED')
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-      transitionType: 'items',
-    })
-
-    expect(result.isFailed()).toBeFalsy()
-    expect(result.getValue()).toEqual('FAILED')
-  })
-
-  it('return error if user uuid is invalid', async () => {
-    const useCase = createUseCase()
-
-    const result = await useCase.execute({
-      userUuid: 'invalid',
-      transitionType: 'items',
-    })
-
-    expect(result.isFailed()).toBeTruthy()
-    expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
-  })
-
-  it('return error if user not found', async () => {
-    const useCase = createUseCase()
-
-    userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-      transitionType: 'items',
-    })
-
-    expect(result.isFailed()).toBeTruthy()
-    expect(result.getError()).toEqual('User not found.')
-  })
-})

+ 0 - 43
packages/auth/src/Domain/UseCase/GetTransitionStatus/GetTransitionStatus.ts

@@ -1,43 +0,0 @@
-import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
-
-import { GetTransitionStatusDTO } from './GetTransitionStatusDTO'
-import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
-import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
-
-export class GetTransitionStatus
-  implements UseCaseInterface<'TO-DO' | 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'>
-{
-  constructor(
-    private transitionStatusRepository: TransitionStatusRepositoryInterface,
-    private userRepository: UserRepositoryInterface,
-  ) {}
-
-  async execute(
-    dto: GetTransitionStatusDTO,
-  ): Promise<Result<'TO-DO' | 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'>> {
-    const userUuidOrError = Uuid.create(dto.userUuid)
-    if (userUuidOrError.isFailed()) {
-      return Result.fail(userUuidOrError.getError())
-    }
-    const userUuid = userUuidOrError.getValue()
-
-    const user = await this.userRepository.findOneByUuid(userUuid)
-    if (user === null) {
-      return Result.fail('User not found.')
-    }
-
-    const roles = await user.roles
-    for (const role of roles) {
-      if (role.name === RoleName.NAMES.TransitionUser) {
-        return Result.ok('FINISHED')
-      }
-    }
-
-    const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid.value, dto.transitionType)
-    if (transitionStatus === null) {
-      return Result.ok('TO-DO')
-    }
-
-    return Result.ok(transitionStatus)
-  }
-}

+ 0 - 4
packages/auth/src/Domain/UseCase/GetTransitionStatus/GetTransitionStatusDTO.ts

@@ -1,4 +0,0 @@
-export interface GetTransitionStatusDTO {
-  userUuid: string
-  transitionType: 'items' | 'revisions'
-}

+ 2 - 39
packages/auth/src/Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus.spec.ts

@@ -12,66 +12,29 @@ describe('UpdateTransitionStatus', () => {
 
   beforeEach(() => {
     transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
-    transitionStatusRepository.removeStatus = jest.fn()
     transitionStatusRepository.updateStatus = jest.fn()
-    transitionStatusRepository.getStatuses = jest.fn().mockResolvedValue([
-      {
-        userUuid: '00000000-0000-0000-0000-000000000000',
-        status: 'STARTED',
-      },
-      {
-        userUuid: '00000000-0000-0000-0000-000000000001',
-        status: 'IN_PROGRESS',
-      },
-      {
-        userUuid: '00000000-0000-0000-0000-000000000002',
-        status: 'FAILED',
-      },
-    ])
 
     roleService = {} as jest.Mocked<RoleServiceInterface>
     roleService.addRoleToUser = jest.fn()
   })
 
-  it('should remove transition status and add TransitionUser role', async () => {
+  it('should add TRANSITION_USER role', async () => {
     const useCase = createUseCase()
 
     const result = await useCase.execute({
       userUuid: '00000000-0000-0000-0000-000000000000',
-      status: 'FINISHED',
+      status: 'VERIFIED',
       transitionType: 'items',
       transitionTimestamp: 123,
     })
 
     expect(result.isFailed()).toBeFalsy()
-    expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith(
-      '00000000-0000-0000-0000-000000000000',
-      'items',
-    )
     expect(roleService.addRoleToUser).toHaveBeenCalledWith(
       Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
     )
   })
 
-  it('should remove transition status', async () => {
-    const useCase = createUseCase()
-
-    const result = await useCase.execute({
-      userUuid: '00000000-0000-0000-0000-000000000000',
-      status: 'FINISHED',
-      transitionType: 'revisions',
-      transitionTimestamp: 123,
-    })
-
-    expect(result.isFailed()).toBeFalsy()
-    expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith(
-      '00000000-0000-0000-0000-000000000000',
-      'revisions',
-    )
-    expect(roleService.addRoleToUser).not.toHaveBeenCalled()
-  })
-
   it('should update transition status', async () => {
     const useCase = createUseCase()
 

+ 2 - 8
packages/auth/src/Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus.ts

@@ -16,15 +16,9 @@ export class UpdateTransitionStatus implements UseCaseInterface<void> {
     }
     const userUuid = userUuidOrError.getValue()
 
-    if (dto.status !== 'FINISHED') {
-      await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
+    await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
 
-      return Result.ok()
-    }
-
-    await this.transitionStatusRepository.removeStatus(dto.userUuid, dto.transitionType)
-
-    if (dto.transitionType === 'items') {
+    if (dto.transitionType === 'items' && dto.status === 'VERIFIED') {
       await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
     }
 

+ 1 - 1
packages/auth/src/Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatusDTO.ts

@@ -2,5 +2,5 @@ export interface UpdateTransitionStatusDTO {
   userUuid: string
   transitionType: 'items' | 'revisions'
   transitionTimestamp: number
-  status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'
+  status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED'
 }

+ 0 - 18
packages/auth/src/Infra/InMemory/InMemoryTransitionStatusRepository.ts

@@ -4,24 +4,6 @@ export class InMemoryTransitionStatusRepository implements TransitionStatusRepos
   private itemStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
   private revisionStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
 
-  async getStatuses(
-    transitionType: 'items' | 'revisions',
-  ): Promise<{ userUuid: string; status: 'STARTED' | 'FAILED' | 'IN_PROGRESS' }[]> {
-    const statuses: { userUuid: string; status: 'STARTED' | 'FAILED' | 'IN_PROGRESS' }[] = []
-
-    if (transitionType === 'items') {
-      for (const [userUuid, status] of this.itemStatuses) {
-        statuses.push({ userUuid, status })
-      }
-    } else {
-      for (const [userUuid, status] of this.revisionStatuses) {
-        statuses.push({ userUuid, status })
-      }
-    }
-
-    return statuses
-  }
-
   async updateStatus(
     userUuid: string,
     transitionType: 'items' | 'revisions',

+ 19 - 33
packages/auth/src/Infra/Redis/RedisTransitionStatusRepository.ts

@@ -7,49 +7,35 @@ export class RedisTransitionStatusRepository implements TransitionStatusReposito
 
   constructor(private redisClient: IORedis.Redis) {}
 
-  async getStatuses(
-    transitionType: 'items' | 'revisions',
-  ): Promise<{ userUuid: string; status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' }[]> {
-    const keys = await this.redisClient.keys(`${this.PREFIX}:${transitionType}:*`)
-    const statuses = await Promise.all(
-      keys.map(async (key) => {
-        const userUuid = key.split(':')[2]
-        const status = (await this.redisClient.get(key)) as 'STARTED' | 'IN_PROGRESS' | 'FAILED'
-        return { userUuid, status }
-      }),
-    )
-
-    return statuses
-  }
-
   async updateStatus(
     userUuid: string,
     transitionType: 'items' | 'revisions',
-    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED',
+    status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED',
   ): Promise<void> {
-    if (status === 'IN_PROGRESS') {
-      await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, 7_200, status)
-    } else if (status === 'STARTED') {
-      await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, 36_000, status)
-    } else {
-      await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status)
+    switch (status) {
+      case 'FAILED':
+      case 'VERIFIED':
+        await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status)
+        break
+      case 'IN_PROGRESS': {
+        const ttl2Hourse = 7_200
+        await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, ttl2Hourse, status)
+        break
+      }
+      default: {
+        const ttl10Hours = 36_000
+        await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, ttl10Hours, status)
+        break
+      }
     }
   }
 
-  async removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void> {
-    await this.redisClient.del(`${this.PREFIX}:${transitionType}:${userUuid}`)
-  }
-
   async getStatus(
     userUuid: string,
     transitionType: 'items' | 'revisions',
-  ): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null> {
-    const status = (await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)) as
-      | 'STARTED'
-      | 'IN_PROGRESS'
-      | 'FAILED'
-      | null
+  ): Promise<'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null> {
+    const status = await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)
 
-    return status
+    return status as 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null
   }
 }

+ 1 - 1
packages/domain-events/src/Domain/Event/TransitionStatusUpdatedEventPayload.ts

@@ -2,5 +2,5 @@ export interface TransitionStatusUpdatedEventPayload {
   userUuid: string
   transitionType: 'items' | 'revisions'
   transitionTimestamp: number
-  status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'
+  status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED'
 }

+ 2 - 2
packages/revisions/src/Domain/Event/DomainEventFactory.ts

@@ -10,7 +10,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     userUuid: string
     transitionType: 'items' | 'revisions'
     transitionTimestamp: number
-    status: 'STARTED' | 'FAILED' | 'FINISHED'
+    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
   }): TransitionStatusUpdatedEvent {
     return {
       type: 'TRANSITION_STATUS_UPDATED',
@@ -20,7 +20,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
           userIdentifier: dto.userUuid,
           userIdentifierType: 'uuid',
         },
-        origin: DomainEventService.SyncingServer,
+        origin: DomainEventService.Revisions,
       },
       payload: dto,
     }

+ 1 - 1
packages/revisions/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -5,6 +5,6 @@ export interface DomainEventFactoryInterface {
     userUuid: string
     transitionType: 'items' | 'revisions'
     transitionTimestamp: number
-    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED'
+    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
   }): TransitionStatusUpdatedEvent
 }

+ 40 - 15
packages/revisions/src/Domain/Handler/TransitionStatusUpdatedEventHandler.ts

@@ -33,24 +33,12 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
     }
 
     if (event.payload.status === 'STARTED' && event.payload.transitionType === 'revisions') {
-      const userUuidOrError = Uuid.create(event.payload.userUuid)
-      if (userUuidOrError.isFailed()) {
-        this.logger.error(
-          `Failed to transition revisions for user ${event.payload.userUuid}: ${userUuidOrError.getError()}`,
-        )
-        await this.domainEventPublisher.publish(
-          this.domainEventFactory.createTransitionStatusUpdatedEvent({
-            userUuid: event.payload.userUuid,
-            status: 'FAILED',
-            transitionType: 'revisions',
-            transitionTimestamp: event.payload.transitionTimestamp,
-          }),
-        )
-
+      const userUuid = await this.getUserUuidFromEvent(event)
+      if (userUuid === null) {
         return
       }
 
-      if (await this.isAlreadyMigrated(userUuidOrError.getValue())) {
+      if (await this.isAlreadyMigrated(userUuid)) {
         await this.domainEventPublisher.publish(
           this.domainEventFactory.createTransitionStatusUpdatedEvent({
             userUuid: event.payload.userUuid,
@@ -102,6 +90,22 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
 
       return
     }
+
+    if (event.payload.status === 'FINISHED' && event.payload.transitionType === 'revisions') {
+      const userUuid = await this.getUserUuidFromEvent(event)
+      if (userUuid === null) {
+        return
+      }
+
+      if (await this.isAlreadyMigrated(userUuid)) {
+        this.domainEventFactory.createTransitionStatusUpdatedEvent({
+          userUuid: event.payload.userUuid,
+          status: 'VERIFIED',
+          transitionType: 'revisions',
+          transitionTimestamp: event.payload.transitionTimestamp,
+        })
+      }
+    }
   }
 
   private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> {
@@ -115,4 +119,25 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
 
     return totalRevisionsCountForUserInPrimary === 0
   }
+
+  private async getUserUuidFromEvent(event: TransitionStatusUpdatedEvent): Promise<Uuid | null> {
+    const userUuidOrError = Uuid.create(event.payload.userUuid)
+    if (userUuidOrError.isFailed()) {
+      this.logger.error(
+        `Failed to transition revisions for user ${event.payload.userUuid}: ${userUuidOrError.getError()}`,
+      )
+      await this.domainEventPublisher.publish(
+        this.domainEventFactory.createTransitionStatusUpdatedEvent({
+          userUuid: event.payload.userUuid,
+          status: 'FAILED',
+          transitionType: 'revisions',
+          transitionTimestamp: event.payload.transitionTimestamp,
+        }),
+      )
+
+      return null
+    }
+
+    return userUuidOrError.getValue()
+  }
 }

+ 0 - 54
packages/syncing-server/bin/transition.ts

@@ -1,54 +0,0 @@
-import 'reflect-metadata'
-
-import { Logger } from 'winston'
-
-import { ContainerConfigLoader } from '../src/Bootstrap/Container'
-import TYPES from '../src/Bootstrap/Types'
-import { Env } from '../src/Bootstrap/Env'
-import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../src/Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
-import { TimerInterface } from '@standardnotes/time'
-
-const inputArgs = process.argv.slice(2)
-const userUuid = inputArgs[0]
-
-const requestTransition = async (
-  triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
-  timer: TimerInterface,
-  logger: Logger,
-): Promise<void> => {
-  const result = await triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
-    userUuid,
-    transitionTimestamp: timer.getTimestampInMicroseconds(),
-  })
-  if (result.isFailed()) {
-    logger.error(`Could not trigger transition for user ${userUuid}: ${result.getError()}`)
-  }
-
-  return
-}
-
-const container = new ContainerConfigLoader('worker')
-void container.load().then((container) => {
-  const env: Env = new Env()
-  env.load()
-
-  const logger: Logger = container.get(TYPES.Sync_Logger)
-
-  logger.info(`Starting transitiong for user ${userUuid} ...`)
-
-  const triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser =
-    container.get(TYPES.Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser)
-  const timer = container.get<TimerInterface>(TYPES.Sync_Timer)
-
-  Promise.resolve(requestTransition(triggerTransitionFromPrimaryToSecondaryDatabaseForUser, timer, logger))
-    .then(() => {
-      logger.info(`Transition triggered for user ${userUuid}`)
-
-      process.exit(0)
-    })
-    .catch((error) => {
-      logger.error(`Could not trigger transition for user ${userUuid}: ${error.message}`)
-
-      process.exit(1)
-    })
-})

+ 0 - 11
packages/syncing-server/docker/entrypoint-transition.js

@@ -1,11 +0,0 @@
-'use strict'
-
-const path = require('path')
-
-const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
-
-const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/transition.js')))
-
-Object.defineProperty(exports, '__esModule', { value: true })
-
-exports.default = index

+ 0 - 6
packages/syncing-server/docker/entrypoint.sh

@@ -14,12 +14,6 @@ case "$COMMAND" in
     node docker/entrypoint-worker.js
     ;;
 
-  'transition' )
-    echo "[Docker] Starting transition Single User..."
-    USER_UUID=$1 && shift 1
-    node docker/entrypoint-transition.js $USER_UUID
-    ;;
-
    * )
     echo "[Docker] Unknown command"
     ;;

+ 1 - 1
packages/syncing-server/src/Domain/Event/DomainEventFactory.ts

@@ -173,7 +173,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     userUuid: string
     transitionType: 'items' | 'revisions'
     transitionTimestamp: number
-    status: 'STARTED' | 'FAILED' | 'FINISHED'
+    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
   }): TransitionStatusUpdatedEvent {
     return {
       type: 'TRANSITION_STATUS_UPDATED',

+ 1 - 1
packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -53,7 +53,7 @@ export interface DomainEventFactoryInterface {
     userUuid: string
     transitionType: 'items' | 'revisions'
     transitionTimestamp: number
-    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED'
+    status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
   }): TransitionStatusUpdatedEvent
   createEmailRequestedEvent(dto: {
     userEmail: string

+ 14 - 1
packages/syncing-server/src/Domain/Handler/TransitionStatusUpdatedEventHandler.ts

@@ -18,7 +18,11 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
   ) {}
 
   async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
-    if (event.payload.status === 'STARTED' && event.payload.transitionType === 'items') {
+    if (event.payload.transitionType !== 'items') {
+      return
+    }
+
+    if (event.payload.status === 'STARTED') {
       if (await this.isAlreadyMigrated(event.payload.userUuid)) {
         await this.domainEventPublisher.publish(
           this.domainEventFactory.createTransitionStatusUpdatedEvent({
@@ -68,6 +72,15 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
           transitionTimestamp: event.payload.transitionTimestamp,
         }),
       )
+    } else if (event.payload.status === 'FINISHED') {
+      if (await this.isAlreadyMigrated(event.payload.userUuid)) {
+        this.domainEventFactory.createTransitionStatusUpdatedEvent({
+          userUuid: event.payload.userUuid,
+          status: 'VERIFIED',
+          transitionType: 'items',
+          transitionTimestamp: event.payload.transitionTimestamp,
+        })
+      }
     }
   }