Browse Source

fix: transfer notifications from auth to syncing-server. (#648)

* fix: transfer notifications from auth to syncing-server.

Co-authored-by: Mo <mo@standardnotes.com>

* fix: add notification to data source init

---------

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko 1 year ago
parent
commit
c288e5d8dc
28 changed files with 364 additions and 85 deletions
  1. 16 0
      packages/auth/migrations/mysql/1688540448428-remove-notifications.ts
  2. 17 0
      packages/auth/migrations/sqlite/1688540623273-remove-notifications.ts
  3. 0 2
      packages/auth/src/Bootstrap/DataSource.ts
  4. 0 3
      packages/domain-core/src/Domain/index.ts
  5. 0 7
      packages/domain-events/src/Domain/Event/NotificationRequestedEvent.ts
  6. 0 5
      packages/domain-events/src/Domain/Event/NotificationRequestedEventPayload.ts
  7. 0 2
      packages/domain-events/src/Domain/index.ts
  8. 16 0
      packages/syncing-server/migrations/mysql/1688540448427-add-notifications.ts
  9. 17 0
      packages/syncing-server/migrations/sqlite/1688540623272-add-notifications.ts
  10. 12 7
      packages/syncing-server/src/Bootstrap/DataSource.ts
  11. 7 0
      packages/syncing-server/src/Bootstrap/MigrationsDataSource.ts
  12. 0 20
      packages/syncing-server/src/Domain/Event/DomainEventFactory.ts
  13. 0 2
      packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts
  14. 2 1
      packages/syncing-server/src/Domain/Notifications/Notification.spec.ts
  15. 0 0
      packages/syncing-server/src/Domain/Notifications/Notification.ts
  16. 3 1
      packages/syncing-server/src/Domain/Notifications/NotificationProps.ts
  17. 5 0
      packages/syncing-server/src/Domain/Notifications/NotificationRepositoryInterface.ts
  18. 0 0
      packages/syncing-server/src/Domain/Notifications/NotificationType.spec.ts
  19. 1 2
      packages/syncing-server/src/Domain/Notifications/NotificationType.ts
  20. 0 0
      packages/syncing-server/src/Domain/Notifications/NotificationTypeProps.ts
  21. 94 0
      packages/syncing-server/src/Domain/UseCase/AddNotificationForUser/AddNotificationForUser.spec.ts
  22. 48 0
      packages/syncing-server/src/Domain/UseCase/AddNotificationForUser/AddNotificationForUser.ts
  23. 6 0
      packages/syncing-server/src/Domain/UseCase/AddNotificationForUser/AddNotificationForUserDTO.ts
  24. 30 19
      packages/syncing-server/src/Domain/UseCase/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts
  25. 14 14
      packages/syncing-server/src/Domain/UseCase/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts
  26. 0 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMNotification.ts
  27. 19 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMNotificationRepository.ts
  28. 57 0
      packages/syncing-server/src/Mapping/Persistence/NotificationPersistenceMapper.ts

+ 16 - 0
packages/auth/migrations/mysql/1688540448428-remove-notifications.ts

@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class RemoveNotifications1688540448428 implements MigrationInterface {
+  name = 'RemoveNotifications1688540448428'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
+    await queryRunner.query('DROP TABLE `notifications`')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
+    )
+  }
+}

+ 17 - 0
packages/auth/migrations/sqlite/1688540623273-remove-notifications.ts

@@ -0,0 +1,17 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class RemoveNotifications1688540623273 implements MigrationInterface {
+  name = 'RemoveNotifications1688540623273'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"')
+    await queryRunner.query('DROP TABLE "notifications"')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
+    )
+    await queryRunner.query('CREATE INDEX "index_notifications_on_user_uuid" ON "notifications" ("user_uuid") ')
+  }
+}

+ 0 - 2
packages/auth/src/Bootstrap/DataSource.ts

@@ -18,7 +18,6 @@ import { TypeORMEmergencyAccessInvitation } from '../Infra/TypeORM/TypeORMEmerge
 import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
-import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
 
 export class AppDataSource {
   private _dataSource: DataSource | undefined
@@ -65,7 +64,6 @@ export class AppDataSource {
         TypeORMAuthenticatorChallenge,
         TypeORMEmergencyAccessInvitation,
         TypeORMCacheEntry,
-        TypeORMNotification,
       ],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrationsRun: true,

+ 0 - 3
packages/domain-core/src/Domain/index.ts

@@ -43,9 +43,6 @@ export * from './Env/AbstractEnv'
 
 export * from './Mapping/MapperInterface'
 
-export * from './Notification/NotificationType'
-export * from './Notification/NotificationTypeProps'
-
 export * from './Service/ServiceConfiguration'
 export * from './Service/ServiceContainer'
 export * from './Service/ServiceContainerInterface'

+ 0 - 7
packages/domain-events/src/Domain/Event/NotificationRequestedEvent.ts

@@ -1,7 +0,0 @@
-import { DomainEventInterface } from './DomainEventInterface'
-import { NotificationRequestedEventPayload } from './NotificationRequestedEventPayload'
-
-export interface NotificationRequestedEvent extends DomainEventInterface {
-  type: 'NOTIFICATION_REQUESTED'
-  payload: NotificationRequestedEventPayload
-}

+ 0 - 5
packages/domain-events/src/Domain/Event/NotificationRequestedEventPayload.ts

@@ -1,5 +0,0 @@
-export interface NotificationRequestedEventPayload {
-  userUuid: string
-  type: string
-  payload: string
-}

+ 0 - 2
packages/domain-events/src/Domain/index.ts

@@ -42,8 +42,6 @@ export * from './Event/ListedAccountRequestedEvent'
 export * from './Event/ListedAccountRequestedEventPayload'
 export * from './Event/MuteEmailsSettingChangedEvent'
 export * from './Event/MuteEmailsSettingChangedEventPayload'
-export * from './Event/NotificationRequestedEvent'
-export * from './Event/NotificationRequestedEventPayload'
 export * from './Event/PaymentFailedEvent'
 export * from './Event/PaymentFailedEventPayload'
 export * from './Event/PaymentSuccessEvent'

+ 16 - 0
packages/syncing-server/migrations/mysql/1688540448427-add-notifications.ts

@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddNotifications1688540448427 implements MigrationInterface {
+  name = 'AddNotifications1688540448427'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
+    )
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
+    await queryRunner.query('DROP TABLE `notifications`')
+  }
+}

+ 17 - 0
packages/syncing-server/migrations/sqlite/1688540623272-add-notifications.ts

@@ -0,0 +1,17 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddNotifications1688540623272 implements MigrationInterface {
+  name = 'AddNotifications1688540623272'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
+    )
+    await queryRunner.query('CREATE INDEX "index_notifications_on_user_uuid" ON "notifications" ("user_uuid") ')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"')
+    await queryRunner.query('DROP TABLE "notifications"')
+  }
+}

+ 12 - 7
packages/syncing-server/src/Bootstrap/DataSource.ts

@@ -1,23 +1,28 @@
 import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } from 'typeorm'
 import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
 import { Item } from '../Domain/Item/Item'
+import { Notification } from '../Domain/Notifications/Notification'
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
 
 export class AppDataSource {
-  private dataSource: DataSource | undefined
+  private _dataSource: DataSource | undefined
 
   constructor(private env: Env) {}
 
   getRepository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): Repository<Entity> {
-    if (!this.dataSource) {
+    if (!this._dataSource) {
       throw new Error('DataSource not initialized')
     }
 
-    return this.dataSource.getRepository(target)
+    return this._dataSource.getRepository(target)
   }
 
   async initialize(): Promise<void> {
+    await this.dataSource.initialize()
+  }
+
+  get dataSource(): DataSource {
     this.env.load()
 
     const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql'
@@ -28,7 +33,7 @@ export class AppDataSource {
 
     const commonDataSourceOptions = {
       maxQueryExecutionTime,
-      entities: [Item],
+      entities: [Item, Notification],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrationsRun: true,
       logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
@@ -72,7 +77,7 @@ export class AppDataSource {
         database: inReplicaMode ? undefined : this.env.get('DB_DATABASE'),
       }
 
-      this.dataSource = new DataSource(mySQLDataSourceOptions)
+      this._dataSource = new DataSource(mySQLDataSourceOptions)
     } else {
       const sqliteDataSourceOptions: SqliteConnectionOptions = {
         ...commonDataSourceOptions,
@@ -80,9 +85,9 @@ export class AppDataSource {
         database: this.env.get('DB_SQLITE_DATABASE_PATH'),
       }
 
-      this.dataSource = new DataSource(sqliteDataSourceOptions)
+      this._dataSource = new DataSource(sqliteDataSourceOptions)
     }
 
-    await this.dataSource.initialize()
+    return this._dataSource
   }
 }

+ 7 - 0
packages/syncing-server/src/Bootstrap/MigrationsDataSource.ts

@@ -0,0 +1,7 @@
+import { AppDataSource } from './DataSource'
+import { Env } from './Env'
+
+const env: Env = new Env()
+env.load()
+
+export const MigrationsDataSource = new AppDataSource(env).dataSource

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

@@ -5,7 +5,6 @@ import {
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemRevisionCreationRequestedEvent,
-  NotificationRequestedEvent,
   RevisionsCopyRequestedEvent,
 } from '@standardnotes/domain-events'
 import { TimerInterface } from '@standardnotes/time'
@@ -14,25 +13,6 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 export class DomainEventFactory implements DomainEventFactoryInterface {
   constructor(private timer: TimerInterface) {}
 
-  createNotificationRequestedEvent(dto: {
-    userUuid: string
-    type: string
-    payload: string
-  }): NotificationRequestedEvent {
-    return {
-      type: 'NOTIFICATION_REQUESTED',
-      createdAt: this.timer.getUTCDate(),
-      meta: {
-        correlation: {
-          userIdentifier: dto.userUuid,
-          userIdentifierType: 'uuid',
-        },
-        origin: DomainEventService.SyncingServer,
-      },
-      payload: dto,
-    }
-  }
-
   createRevisionsCopyRequestedEvent(
     userUuid: string,
     dto: {

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

@@ -3,12 +3,10 @@ import {
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemRevisionCreationRequestedEvent,
-  NotificationRequestedEvent,
   RevisionsCopyRequestedEvent,
 } from '@standardnotes/domain-events'
 
 export interface DomainEventFactoryInterface {
-  createNotificationRequestedEvent(dto: { userUuid: string; type: string; payload: string }): NotificationRequestedEvent
   createEmailRequestedEvent(dto: {
     userEmail: string
     messageIdentifier: string

+ 2 - 1
packages/auth/src/Domain/Notifications/Notification.spec.ts → packages/syncing-server/src/Domain/Notifications/Notification.spec.ts

@@ -1,6 +1,7 @@
-import { NotificationType, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
 
 import { Notification } from './Notification'
+import { NotificationType } from './NotificationType'
 
 describe('Notification', () => {
   it('should create an entity', () => {

+ 0 - 0
packages/auth/src/Domain/Notifications/Notification.ts → packages/syncing-server/src/Domain/Notifications/Notification.ts


+ 3 - 1
packages/auth/src/Domain/Notifications/NotificationProps.ts → packages/syncing-server/src/Domain/Notifications/NotificationProps.ts

@@ -1,4 +1,6 @@
-import { NotificationType, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { NotificationType } from './NotificationType'
 
 export interface NotificationProps {
   userUuid: Uuid

+ 5 - 0
packages/syncing-server/src/Domain/Notifications/NotificationRepositoryInterface.ts

@@ -0,0 +1,5 @@
+import { Notification } from './Notification'
+
+export interface NotificationRepositoryInterface {
+  save(notification: Notification): Promise<void>
+}

+ 0 - 0
packages/domain-core/src/Domain/Notification/NotificationType.spec.ts → packages/syncing-server/src/Domain/Notifications/NotificationType.spec.ts


+ 1 - 2
packages/domain-core/src/Domain/Notification/NotificationType.ts → packages/syncing-server/src/Domain/Notifications/NotificationType.ts

@@ -1,5 +1,4 @@
-import { Result } from '../Core/Result'
-import { ValueObject } from '../Core/ValueObject'
+import { ValueObject, Result } from '@standardnotes/domain-core'
 
 import { NotificationTypeProps } from './NotificationTypeProps'
 

+ 0 - 0
packages/domain-core/src/Domain/Notification/NotificationTypeProps.ts → packages/syncing-server/src/Domain/Notifications/NotificationTypeProps.ts


+ 94 - 0
packages/syncing-server/src/Domain/UseCase/AddNotificationForUser/AddNotificationForUser.spec.ts

@@ -0,0 +1,94 @@
+import { TimerInterface } from '@standardnotes/time'
+import { Result } from '@standardnotes/domain-core'
+
+import { NotificationRepositoryInterface } from '../../Notifications/NotificationRepositoryInterface'
+import { Notification } from '../../Notifications/Notification'
+import { AddNotificationForUser } from './AddNotificationForUser'
+import { NotificationType } from '../../Notifications/NotificationType'
+
+describe('AddNotificationForUser', () => {
+  let notificationRepository: NotificationRepositoryInterface
+  let timer: TimerInterface
+
+  const createUseCase = () => new AddNotificationForUser(notificationRepository, timer)
+
+  beforeEach(() => {
+    notificationRepository = {} as jest.Mocked<NotificationRepositoryInterface>
+    notificationRepository.save = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
+  })
+
+  it('should save notification', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
+      type: NotificationType.TYPES.RemovedFromSharedVault,
+      payload: 'payload',
+      version: '1.0',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+  })
+
+  it('should return error if user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: 'invalid',
+      type: NotificationType.TYPES.RemovedFromSharedVault,
+      payload: 'payload',
+      version: '1.0',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if notification type is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
+      type: 'invalid',
+      payload: 'payload',
+      version: '1.0',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if notification payload is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
+      type: NotificationType.TYPES.RemovedFromSharedVault,
+      payload: '',
+      version: '1.0',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if notification could not be created', async () => {
+    const mock = jest.spyOn(Notification, 'create')
+    mock.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
+      type: NotificationType.TYPES.RemovedFromSharedVault,
+      payload: 'payload',
+      version: '1.0',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+
+    mock.mockRestore()
+  })
+})

+ 48 - 0
packages/syncing-server/src/Domain/UseCase/AddNotificationForUser/AddNotificationForUser.ts

@@ -0,0 +1,48 @@
+import { Result, Timestamps, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+
+import { AddNotificationForUserDTO } from './AddNotificationForUserDTO'
+import { NotificationRepositoryInterface } from '../../Notifications/NotificationRepositoryInterface'
+import { Notification } from '../../Notifications/Notification'
+import { NotificationType } from '../../Notifications/NotificationType'
+
+export class AddNotificationForUser implements UseCaseInterface<Notification> {
+  constructor(private notificationRepository: NotificationRepositoryInterface, private timer: TimerInterface) {}
+
+  async execute(dto: AddNotificationForUserDTO): Promise<Result<Notification>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const typeOrError = NotificationType.create(dto.type)
+    if (typeOrError.isFailed()) {
+      return Result.fail(typeOrError.getError())
+    }
+    const type = typeOrError.getValue()
+
+    const paylodNotEmptyValidationResult = Validator.isNotEmpty(dto.payload)
+    if (paylodNotEmptyValidationResult.isFailed()) {
+      return Result.fail(paylodNotEmptyValidationResult.getError())
+    }
+
+    const notificationOrError = Notification.create({
+      userUuid,
+      type,
+      payload: dto.payload,
+      timestamps: Timestamps.create(
+        this.timer.getTimestampInMicroseconds(),
+        this.timer.getTimestampInMicroseconds(),
+      ).getValue(),
+    })
+    if (notificationOrError.isFailed()) {
+      return Result.fail(notificationOrError.getError())
+    }
+    const notification = notificationOrError.getValue()
+
+    await this.notificationRepository.save(notification)
+
+    return Result.ok(notification)
+  }
+}

+ 6 - 0
packages/syncing-server/src/Domain/UseCase/AddNotificationForUser/AddNotificationForUserDTO.ts

@@ -0,0 +1,6 @@
+export interface AddNotificationForUserDTO {
+  version: string
+  type: string
+  userUuid: string
+  payload: string
+}

+ 30 - 19
packages/syncing-server/src/Domain/UseCase/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts

@@ -1,29 +1,22 @@
-import { Uuid, Timestamps } from '@standardnotes/domain-core'
-import { DomainEventPublisherInterface, NotificationRequestedEvent } from '@standardnotes/domain-events'
+import { Uuid, Timestamps, Result } from '@standardnotes/domain-core'
 
-import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
 import { SharedVault } from '../../SharedVault/SharedVault'
 import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
 import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
 import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { RemoveUserFromSharedVault } from './RemoveUserFromSharedVault'
+import { AddNotificationForUser } from '../AddNotificationForUser/AddNotificationForUser'
 
 describe('RemoveUserFromSharedVault', () => {
   let sharedVaultRepository: SharedVaultRepositoryInterface
   let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
-  let domainEventPublisher: DomainEventPublisherInterface
-  let domainEventFactory: DomainEventFactoryInterface
+  let addNotificationForUser: AddNotificationForUser
   let sharedVault: SharedVault
   let sharedVaultUser: SharedVaultUser
 
   const createUseCase = () =>
-    new RemoveUserFromSharedVault(
-      sharedVaultUserRepository,
-      sharedVaultRepository,
-      domainEventFactory,
-      domainEventPublisher,
-    )
+    new RemoveUserFromSharedVault(sharedVaultUserRepository, sharedVaultRepository, addNotificationForUser)
 
   beforeEach(() => {
     sharedVault = SharedVault.create({
@@ -46,13 +39,8 @@ describe('RemoveUserFromSharedVault', () => {
     sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
     sharedVaultUserRepository.remove = jest.fn()
 
-    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
-    domainEventPublisher.publish = jest.fn()
-
-    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
-    domainEventFactory.createNotificationRequestedEvent = jest
-      .fn()
-      .mockReturnValue({} as jest.Mocked<NotificationRequestedEvent>)
+    addNotificationForUser = {} as jest.Mocked<AddNotificationForUser>
+    addNotificationForUser.execute = jest.fn().mockReturnValue(Result.ok())
   })
 
   it('should remove user from shared vault', async () => {
@@ -64,7 +52,6 @@ describe('RemoveUserFromSharedVault', () => {
     })
 
     expect(sharedVaultUserRepository.remove).toHaveBeenCalledWith(sharedVaultUser)
-    expect(domainEventPublisher.publish).toHaveBeenCalled()
   })
 
   it('should return error when shared vault is not found', async () => {
@@ -162,4 +149,28 @@ describe('RemoveUserFromSharedVault', () => {
     expect(result.isFailed()).toBe(true)
     expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
   })
+
+  it('should add notification for user', async () => {
+    const useCase = createUseCase()
+    await useCase.execute({
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      userUuid: '00000000-0000-0000-0000-000000000001',
+    })
+
+    expect(addNotificationForUser.execute).toHaveBeenCalled()
+  })
+
+  it('should return error if notification could not be added', async () => {
+    addNotificationForUser.execute = jest.fn().mockResolvedValue(Result.fail('Could not add notification'))
+
+    const useCase = createUseCase()
+    const result = await useCase.execute({
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      userUuid: '00000000-0000-0000-0000-000000000001',
+    })
+
+    expect(result.isFailed()).toBe(true)
+  })
 })

+ 14 - 14
packages/syncing-server/src/Domain/UseCase/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts

@@ -1,17 +1,16 @@
-import { NotificationType, Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 
 import { RemoveUserFromSharedVaultDTO } from './RemoveUserFromSharedVaultDTO'
 import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
-import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
-import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { AddNotificationForUser } from '../AddNotificationForUser/AddNotificationForUser'
+import { NotificationType } from '../../Notifications/NotificationType'
 
 export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
   constructor(
     private sharedVaultUsersRepository: SharedVaultUserRepositoryInterface,
     private sharedVaultRepository: SharedVaultRepositoryInterface,
-    private domainEventFactory: DomainEventFactoryInterface,
-    private domainEventPublisher: DomainEventPublisherInterface,
+    private addNotificationForUser: AddNotificationForUser,
   ) {}
 
   async execute(dto: RemoveUserFromSharedVaultDTO): Promise<Result<void>> {
@@ -58,16 +57,17 @@ export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
 
     await this.sharedVaultUsersRepository.remove(sharedVaultUser)
 
-    await this.domainEventPublisher.publish(
-      this.domainEventFactory.createNotificationRequestedEvent({
-        type: NotificationType.TYPES.RemovedFromSharedVault,
-        userUuid: sharedVaultUser.props.userUuid.value,
-        payload: JSON.stringify({
-          sharedVaultUuid: sharedVault.id.toString(),
-          version: '1.0',
-        }),
+    const result = await this.addNotificationForUser.execute({
+      userUuid: sharedVaultUser.props.userUuid.value,
+      type: NotificationType.TYPES.RemovedFromSharedVault,
+      payload: JSON.stringify({
+        sharedVaultUuid: sharedVault.id.toString(),
       }),
-    )
+      version: '1.0',
+    })
+    if (result.isFailed()) {
+      return Result.fail(result.getError())
+    }
 
     return Result.ok()
   }

+ 0 - 0
packages/auth/src/Infra/TypeORM/TypeORMNotification.ts → packages/syncing-server/src/Infra/TypeORM/TypeORMNotification.ts


+ 19 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMNotificationRepository.ts

@@ -0,0 +1,19 @@
+import { Repository } from 'typeorm'
+import { MapperInterface } from '@standardnotes/domain-core'
+
+import { NotificationRepositoryInterface } from '../../Domain/Notifications/NotificationRepositoryInterface'
+import { TypeORMNotification } from './TypeORMNotification'
+import { Notification } from '../../Domain/Notifications/Notification'
+
+export class TypeORMNotificationRepository implements NotificationRepositoryInterface {
+  constructor(
+    private ormRepository: Repository<TypeORMNotification>,
+    private mapper: MapperInterface<Notification, TypeORMNotification>,
+  ) {}
+
+  async save(sharedVault: Notification): Promise<void> {
+    const persistence = this.mapper.toProjection(sharedVault)
+
+    await this.ormRepository.save(persistence)
+  }
+}

+ 57 - 0
packages/syncing-server/src/Mapping/Persistence/NotificationPersistenceMapper.ts

@@ -0,0 +1,57 @@
+import { Timestamps, MapperInterface, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
+
+import { Notification } from '../../Domain/Notifications/Notification'
+
+import { TypeORMNotification } from '../../Infra/TypeORM/TypeORMNotification'
+import { NotificationType } from '../../Domain/Notifications/NotificationType'
+
+export class NotificationPersistenceMapper implements MapperInterface<Notification, TypeORMNotification> {
+  toDomain(projection: TypeORMNotification): Notification {
+    const userUuidOrError = Uuid.create(projection.userUuid)
+    if (userUuidOrError.isFailed()) {
+      throw new Error(`Failed to create notification from projection: ${userUuidOrError.getError()}`)
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
+    if (timestampsOrError.isFailed()) {
+      throw new Error(`Failed to create notification from projection: ${timestampsOrError.getError()}`)
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    const typeOrError = NotificationType.create(projection.type)
+    if (typeOrError.isFailed()) {
+      throw new Error(`Failed to create notification from projection: ${typeOrError.getError()}`)
+    }
+    const type = typeOrError.getValue()
+
+    const notificationOrError = Notification.create(
+      {
+        userUuid,
+        payload: projection.payload,
+        type,
+        timestamps,
+      },
+      new UniqueEntityId(projection.uuid),
+    )
+    if (notificationOrError.isFailed()) {
+      throw new Error(`Failed to create notification from projection: ${notificationOrError.getError()}`)
+    }
+    const notification = notificationOrError.getValue()
+
+    return notification
+  }
+
+  toProjection(domain: Notification): TypeORMNotification {
+    const typeorm = new TypeORMNotification()
+
+    typeorm.uuid = domain.id.toString()
+    typeorm.userUuid = domain.props.userUuid.value
+    typeorm.payload = domain.props.payload
+    typeorm.type = domain.props.type.value
+    typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
+    typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
+
+    return typeorm
+  }
+}