From efa4d7fc6007ef668e3de3b04853ac11b2d13c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Thu, 20 Jul 2023 11:52:45 +0200 Subject: [PATCH] feat(syncing-server): add shared vaults, invites, messages and notifications to sync response (#665) * feat(syncing-server): add shared vaults, invites, messages and notifications to sync response * fix(syncing-server): migration timestamps * fix: issue with migrations for notifications --- .../v1/SharedVaultUsersController.ts | 4 +- .../src/Service/Resolver/EndpointResolver.ts | 2 +- .../mysql/1688540448427-add-notifications.ts | 16 --- .../1688540448428-remove-notifications.ts | 16 --- .../sqlite/1688540623272-add-notifications.ts | 17 --- .../1688540623273-remove-notifications.ts | 17 --- .../mysql/1688540448427-add-notifications.ts | 16 --- .../mysql/1689671563304-add-notifications.ts | 16 +++ ....ts => 1689672099828-add-notifications.ts} | 6 +- .../syncing-server/src/Bootstrap/Container.ts | 46 +++++-- .../syncing-server/src/Bootstrap/Types.ts | 2 + .../Item/SyncResponse/SyncResponse20200115.ts | 14 +- .../SyncResponseFactory20161215.spec.ts | 8 ++ .../SyncResponseFactory20200115.spec.ts | 56 +++++++- .../SyncResponseFactory20200115.ts | 36 +++++ .../Message/MessageRepositoryInterface.ts | 1 + .../NotificationRepositoryInterface.ts | 4 + .../SharedVaultInviteRepositoryInterface.ts | 1 + .../GetMessagesSentToUser.spec.ts | 11 ++ .../GetMessagesSentToUser.ts | 6 +- .../GetMessagesSentToUserDTO.ts | 1 + .../GetUserNotifications.spec.ts | 43 ++++++ .../GetUserNotifications.ts | 22 ++++ .../GetUserNotificationsDTO.ts | 4 + .../GetSharedVaultInvitesSentToUser.spec.ts | 12 ++ .../GetSharedVaultInvitesSentToUser.ts | 4 + .../GetSharedVaultInvitesSentToUserDTO.ts | 1 + .../UseCase/Syncing/GetItems/GetItems.spec.ts | 5 + .../UseCase/Syncing/GetItems/GetItems.ts | 1 + .../Syncing/GetItems/GetItemsResult.ts | 1 + .../Syncing/SyncItems/SyncItems.spec.ts | 123 +++++++++++++++++- .../UseCase/Syncing/SyncItems/SyncItems.ts | 48 +++++++ .../Syncing/SyncItems/SyncItemsResponse.ts | 8 ++ .../HomeServerSharedVaultInvitesController.ts | 2 +- .../Infra/TypeORM/TypeORMMessageRepository.ts | 14 ++ .../TypeORM/TypeORMNotificationRepository.ts | 27 +++- .../TypeORMSharedVaultInviteRepository.ts | 14 ++ .../Mapping/Http/NotificationHttpMapper.ts | 21 +++ .../Http/NotificationHttpRepresentation.ts | 8 ++ .../Http/SharedVaultInviteHttpMapper.ts | 2 +- .../SharedVaultInviteHttpRepresentation.ts | 2 +- 41 files changed, 545 insertions(+), 113 deletions(-) delete mode 100644 packages/auth/migrations/mysql/1688540448427-add-notifications.ts delete mode 100644 packages/auth/migrations/mysql/1688540448428-remove-notifications.ts delete mode 100644 packages/auth/migrations/sqlite/1688540623272-add-notifications.ts delete mode 100644 packages/auth/migrations/sqlite/1688540623273-remove-notifications.ts delete mode 100644 packages/syncing-server/migrations/mysql/1688540448427-add-notifications.ts create mode 100644 packages/syncing-server/migrations/mysql/1689671563304-add-notifications.ts rename packages/syncing-server/migrations/sqlite/{1688540623272-add-notifications.ts => 1689672099828-add-notifications.ts} (57%) create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotificationsDTO.ts create mode 100644 packages/syncing-server/src/Mapping/Http/NotificationHttpMapper.ts create mode 100644 packages/syncing-server/src/Mapping/Http/NotificationHttpRepresentation.ts diff --git a/packages/api-gateway/src/Controller/v1/SharedVaultUsersController.ts b/packages/api-gateway/src/Controller/v1/SharedVaultUsersController.ts index 6b554c54b..38c334b19 100644 --- a/packages/api-gateway/src/Controller/v1/SharedVaultUsersController.ts +++ b/packages/api-gateway/src/Controller/v1/SharedVaultUsersController.ts @@ -21,7 +21,7 @@ export class SharedVaultUsersController extends BaseHttpController { response, this.endpointResolver.resolveEndpointOrMethodIdentifier( 'GET', - '/shared-vaults/:sharedVaultUuid/users', + 'shared-vaults/:sharedVaultUuid/users', request.params.sharedVaultUuid, ), request.body, @@ -35,7 +35,7 @@ export class SharedVaultUsersController extends BaseHttpController { response, this.endpointResolver.resolveEndpointOrMethodIdentifier( 'DELETE', - '/shared-vaults/:sharedVaultUuid/users/:userUuid', + 'shared-vaults/:sharedVaultUuid/users/:userUuid', request.params.sharedVaultUuid, request.params.userUuid, ), diff --git a/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts b/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts index c73c28048..a32475873 100644 --- a/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts +++ b/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts @@ -100,7 +100,7 @@ export class EndpointResolver implements EndpointResolverInterface { const identifier = this.endpointToIdentifierMap.get(`[${method}]:${endpoint}`) if (!identifier) { - throw new Error(`Endpoint ${endpoint} not found`) + throw new Error(`Endpoint [${method}]:${endpoint} not found`) } return identifier diff --git a/packages/auth/migrations/mysql/1688540448427-add-notifications.ts b/packages/auth/migrations/mysql/1688540448427-add-notifications.ts deleted file mode 100644 index f79e707d8..000000000 --- a/packages/auth/migrations/mysql/1688540448427-add-notifications.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class AddNotifications1688540448427 implements MigrationInterface { - name = 'AddNotifications1688540448427' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`') - await queryRunner.query('DROP TABLE `notifications`') - } -} diff --git a/packages/auth/migrations/mysql/1688540448428-remove-notifications.ts b/packages/auth/migrations/mysql/1688540448428-remove-notifications.ts deleted file mode 100644 index 2aeeb41bb..000000000 --- a/packages/auth/migrations/mysql/1688540448428-remove-notifications.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class RemoveNotifications1688540448428 implements MigrationInterface { - name = 'RemoveNotifications1688540448428' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`') - await queryRunner.query('DROP TABLE `notifications`') - } - - public async down(queryRunner: QueryRunner): Promise { - 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', - ) - } -} diff --git a/packages/auth/migrations/sqlite/1688540623272-add-notifications.ts b/packages/auth/migrations/sqlite/1688540623272-add-notifications.ts deleted file mode 100644 index 5fa10601f..000000000 --- a/packages/auth/migrations/sqlite/1688540623272-add-notifications.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class AddNotifications1688540623272 implements MigrationInterface { - name = 'AddNotifications1688540623272' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"') - await queryRunner.query('DROP TABLE "notifications"') - } -} diff --git a/packages/auth/migrations/sqlite/1688540623273-remove-notifications.ts b/packages/auth/migrations/sqlite/1688540623273-remove-notifications.ts deleted file mode 100644 index 658f745ae..000000000 --- a/packages/auth/migrations/sqlite/1688540623273-remove-notifications.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class RemoveNotifications1688540623273 implements MigrationInterface { - name = 'RemoveNotifications1688540623273' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"') - await queryRunner.query('DROP TABLE "notifications"') - } - - public async down(queryRunner: QueryRunner): Promise { - 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") ') - } -} diff --git a/packages/syncing-server/migrations/mysql/1688540448427-add-notifications.ts b/packages/syncing-server/migrations/mysql/1688540448427-add-notifications.ts deleted file mode 100644 index f79e707d8..000000000 --- a/packages/syncing-server/migrations/mysql/1688540448427-add-notifications.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class AddNotifications1688540448427 implements MigrationInterface { - name = 'AddNotifications1688540448427' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`') - await queryRunner.query('DROP TABLE `notifications`') - } -} diff --git a/packages/syncing-server/migrations/mysql/1689671563304-add-notifications.ts b/packages/syncing-server/migrations/mysql/1689671563304-add-notifications.ts new file mode 100644 index 000000000..186f7d001 --- /dev/null +++ b/packages/syncing-server/migrations/mysql/1689671563304-add-notifications.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddNotifications1689671563304 implements MigrationInterface { + name = 'AddNotifications1689671563304' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'CREATE TABLE IF NOT EXISTS `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 { + await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`') + await queryRunner.query('DROP TABLE `notifications`') + } +} diff --git a/packages/syncing-server/migrations/sqlite/1688540623272-add-notifications.ts b/packages/syncing-server/migrations/sqlite/1689672099828-add-notifications.ts similarity index 57% rename from packages/syncing-server/migrations/sqlite/1688540623272-add-notifications.ts rename to packages/syncing-server/migrations/sqlite/1689672099828-add-notifications.ts index 5fa10601f..6bfd27fc5 100644 --- a/packages/syncing-server/migrations/sqlite/1688540623272-add-notifications.ts +++ b/packages/syncing-server/migrations/sqlite/1689672099828-add-notifications.ts @@ -1,11 +1,11 @@ import { MigrationInterface, QueryRunner } from 'typeorm' -export class AddNotifications1688540623272 implements MigrationInterface { - name = 'AddNotifications1688540623272' +export class AddNotifications1689672099828 implements MigrationInterface { + name = 'AddNotifications1689672099828' public async up(queryRunner: QueryRunner): Promise { 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)', + 'CREATE TABLE IF NOT EXISTS "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") ') } diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index b8d7b3c88..8b51ef5e5 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -147,6 +147,9 @@ import { DeleteAllMessagesSentToUser } from '../Domain/UseCase/Messaging/DeleteA import { DeleteMessage } from '../Domain/UseCase/Messaging/DeleteMessage/DeleteMessage' import { MessageHttpRepresentation } from '../Mapping/Http/MessageHttpRepresentation' import { MessageHttpMapper } from '../Mapping/Http/MessageHttpMapper' +import { GetUserNotifications } from '../Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications' +import { NotificationHttpMapper } from '../Mapping/Http/NotificationHttpMapper' +import { NotificationHttpRepresentation } from '../Mapping/Http/NotificationHttpRepresentation' export class ContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -340,6 +343,9 @@ export class ContainerConfigLoader { container .bind>(TYPES.Sync_MessageHttpMapper) .toConstantValue(new MessageHttpMapper()) + container + .bind>(TYPES.Sync_NotificationHttpMapper) + .toConstantValue(new NotificationHttpMapper()) // ORM container @@ -550,6 +556,24 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_Logger), ), ) + container + .bind(TYPES.Sync_GetUserNotifications) + .toConstantValue(new GetUserNotifications(container.get(TYPES.Sync_NotificationRepository))) + container + .bind(TYPES.Sync_GetSharedVaults) + .toConstantValue( + new GetSharedVaults( + container.get(TYPES.Sync_SharedVaultUserRepository), + container.get(TYPES.Sync_SharedVaultRepository), + ), + ) + container + .bind(TYPES.Sync_GetSharedVaultInvitesSentToUser) + .toConstantValue(new GetSharedVaultInvitesSentToUser(container.get(TYPES.Sync_SharedVaultInviteRepository))) + container + .bind(TYPES.Sync_GetMessagesSentToUser) + .toConstantValue(new GetMessagesSentToUser(container.get(TYPES.Sync_MessageRepository))) + container .bind(TYPES.Sync_SyncItems) .toConstantValue( @@ -557,6 +581,10 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_ItemRepository), container.get(TYPES.Sync_GetItems), container.get(TYPES.Sync_SaveItems), + container.get(TYPES.Sync_GetSharedVaults), + container.get(TYPES.Sync_GetSharedVaultInvitesSentToUser), + container.get(TYPES.Sync_GetMessagesSentToUser), + container.get(TYPES.Sync_GetUserNotifications), ), ) container.bind(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => { @@ -621,9 +649,6 @@ export class ContainerConfigLoader { container .bind(TYPES.Sync_GetSharedVaultInvitesSentByUser) .toConstantValue(new GetSharedVaultInvitesSentByUser(container.get(TYPES.Sync_SharedVaultInviteRepository))) - container - .bind(TYPES.Sync_GetSharedVaultInvitesSentToUser) - .toConstantValue(new GetSharedVaultInvitesSentToUser(container.get(TYPES.Sync_SharedVaultInviteRepository))) container .bind(TYPES.Sync_GetSharedVaultUsers) .toConstantValue( @@ -646,14 +671,6 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_AddNotificationForUser), ), ) - container - .bind(TYPES.Sync_GetSharedVaults) - .toConstantValue( - new GetSharedVaults( - container.get(TYPES.Sync_SharedVaultUserRepository), - container.get(TYPES.Sync_SharedVaultRepository), - ), - ) container .bind(TYPES.Sync_CreateSharedVault) .toConstantValue( @@ -683,9 +700,6 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_VALET_TOKEN_TTL), ), ) - container - .bind(TYPES.Sync_GetMessagesSentToUser) - .toConstantValue(new GetMessagesSentToUser(container.get(TYPES.Sync_MessageRepository))) container .bind(TYPES.Sync_GetMessagesSentByUser) .toConstantValue(new GetMessagesSentByUser(container.get(TYPES.Sync_MessageRepository))) @@ -717,6 +731,10 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_ItemHttpMapper), container.get(TYPES.Sync_ItemConflictHttpMapper), container.get(TYPES.Sync_SavedItemHttpMapper), + container.get(TYPES.Sync_SharedVaultHttpMapper), + container.get(TYPES.Sync_SharedVaultInviteHttpMapper), + container.get(TYPES.Sync_MessageHttpMapper), + container.get(TYPES.Sync_NotificationHttpMapper), ), ) container diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index c2a859e26..65261824e 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -75,6 +75,7 @@ const TYPES = { Sync_UpdateExistingItem: Symbol.for('Sync_UpdateExistingItem'), Sync_GetItems: Symbol.for('Sync_GetItems'), Sync_SaveItems: Symbol.for('Sync_SaveItems'), + Sync_GetUserNotifications: Symbol.for('Sync_GetUserNotifications'), // Handlers Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'), Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'), @@ -113,6 +114,7 @@ const TYPES = { Sync_SharedVaultInviteHttpMapper: Symbol.for('Sync_SharedVaultInviteHttpMapper'), Sync_MessagePersistenceMapper: Symbol.for('Sync_MessagePersistenceMapper'), Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'), + Sync_NotificationHttpMapper: Symbol.for('Sync_NotificationHttpMapper'), Sync_ItemPersistenceMapper: Symbol.for('Sync_ItemPersistenceMapper'), Sync_ItemHttpMapper: Symbol.for('Sync_ItemHttpMapper'), Sync_ItemHashHttpMapper: Symbol.for('Sync_ItemHashHttpMapper'), diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts index 007e63899..caf30df8d 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts @@ -1,11 +1,19 @@ import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation' import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' +import { MessageHttpRepresentation } from '../../../Mapping/Http/MessageHttpRepresentation' +import { NotificationHttpRepresentation } from '../../../Mapping/Http/NotificationHttpRepresentation' import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation' +import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation' +import { SharedVaultInviteHttpRepresentation } from '../../../Mapping/Http/SharedVaultInviteHttpRepresentation' export type SyncResponse20200115 = { - retrieved_items: Array - saved_items: Array - conflicts: Array + retrieved_items: ItemHttpRepresentation[] + saved_items: SavedItemHttpRepresentation[] + conflicts: ItemConflictHttpRepresentation[] sync_token: string cursor_token?: string + messages: MessageHttpRepresentation[] + shared_vaults: SharedVaultHttpRepresentation[] + shared_vault_invites: SharedVaultInviteHttpRepresentation[] + notifications: NotificationHttpRepresentation[] } diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts index 0c191e461..1467d7855 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts @@ -88,6 +88,10 @@ describe('SyncResponseFactory20161215', () => { ], syncToken: 'sync-test', cursorToken: 'cursor-test', + sharedVaults: [], + sharedVaultInvites: [], + messages: [], + notifications: [], }), ).toEqual({ retrieved_items: [item1Projection], @@ -133,6 +137,10 @@ describe('SyncResponseFactory20161215', () => { ], syncToken: 'sync-test', cursorToken: 'cursor-test', + sharedVaults: [], + sharedVaultInvites: [], + messages: [], + notifications: [], }), ).toEqual({ retrieved_items: [], diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts index edc5a7505..73f0b65d0 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts @@ -8,6 +8,14 @@ import { SyncResponseFactory20200115 } from './SyncResponseFactory20200115' import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation' import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation' +import { MessageHttpRepresentation } from '../../../Mapping/Http/MessageHttpRepresentation' +import { NotificationHttpRepresentation } from '../../../Mapping/Http/NotificationHttpRepresentation' +import { Notification } from '../../Notifications/Notification' +import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation' +import { SharedVaultInviteHttpRepresentation } from '../../../Mapping/Http/SharedVaultInviteHttpRepresentation' +import { Message } from '../../Message/Message' +import { SharedVault } from '../../SharedVault/SharedVault' +import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite' describe('SyncResponseFactory20200115', () => { let itemMapper: MapperInterface @@ -19,8 +27,25 @@ describe('SyncResponseFactory20200115', () => { let item1: Item let item2: Item let itemConflict: ItemConflict + let sharedVault: SharedVault + let sharedVaultInvite: SharedVaultInvite + let message: Message + let notification: Notification + let sharedVaultMapper: MapperInterface + let sharedVaultInvitesMapper: MapperInterface + let messageMapper: MapperInterface + let notificationMapper: MapperInterface - const createFactory = () => new SyncResponseFactory20200115(itemMapper, itemConflictMapper, savedItemMapper) + const createFactory = () => + new SyncResponseFactory20200115( + itemMapper, + itemConflictMapper, + savedItemMapper, + sharedVaultMapper, + sharedVaultInvitesMapper, + messageMapper, + notificationMapper, + ) beforeEach(() => { itemProjection = { @@ -45,6 +70,27 @@ describe('SyncResponseFactory20200115', () => { item2 = {} as jest.Mocked itemConflict = {} as jest.Mocked + + sharedVaultMapper = {} as jest.Mocked> + sharedVaultMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked) + + sharedVaultInvitesMapper = {} as jest.Mocked< + MapperInterface + > + sharedVaultInvitesMapper.toProjection = jest + .fn() + .mockReturnValue({} as jest.Mocked) + + messageMapper = {} as jest.Mocked> + messageMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked) + + notificationMapper = {} as jest.Mocked> + notificationMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked) + + sharedVault = {} as jest.Mocked + sharedVaultInvite = {} as jest.Mocked + message = {} as jest.Mocked + notification = {} as jest.Mocked }) it('should turn sync items response into a sync response for API Version 20200115', async () => { @@ -55,6 +101,10 @@ describe('SyncResponseFactory20200115', () => { conflicts: [itemConflict], syncToken: 'sync-test', cursorToken: 'cursor-test', + sharedVaults: [sharedVault], + sharedVaultInvites: [sharedVaultInvite], + messages: [message], + notifications: [notification], }), ).toEqual({ retrieved_items: [itemProjection], @@ -62,6 +112,10 @@ describe('SyncResponseFactory20200115', () => { conflicts: [itemConflictProjection], sync_token: 'sync-test', cursor_token: 'cursor-test', + shared_vaults: [{} as jest.Mocked], + shared_vault_invites: [{} as jest.Mocked], + messages: [{} as jest.Mocked], + notifications: [{} as jest.Mocked], }) }) }) diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts index e5a38ee7b..95418c6de 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts @@ -8,12 +8,24 @@ import { SyncItemsResponse } from '../../UseCase/Syncing/SyncItems/SyncItemsResp import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation' import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation' +import { SharedVault } from '../../SharedVault/SharedVault' +import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation' +import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite' +import { SharedVaultInviteHttpRepresentation } from '../../../Mapping/Http/SharedVaultInviteHttpRepresentation' +import { Message } from '../../Message/Message' +import { MessageHttpRepresentation } from '../../../Mapping/Http/MessageHttpRepresentation' +import { Notification } from '../../Notifications/Notification' +import { NotificationHttpRepresentation } from '../../../Mapping/Http/NotificationHttpRepresentation' export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface { constructor( private httpMapper: MapperInterface, private itemConflictMapper: MapperInterface, private savedItemMapper: MapperInterface, + private sharedVaultMapper: MapperInterface, + private sharedVaultInvitesMapper: MapperInterface, + private messageMapper: MapperInterface, + private notificationMapper: MapperInterface, ) {} async createResponse(syncItemsResponse: SyncItemsResponse): Promise { @@ -32,12 +44,36 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface conflicts.push(this.itemConflictMapper.toProjection(itemConflict)) } + const sharedVaults = [] + for (const sharedVault of syncItemsResponse.sharedVaults) { + sharedVaults.push(this.sharedVaultMapper.toProjection(sharedVault)) + } + + const sharedVaultInvites = [] + for (const sharedVaultInvite of syncItemsResponse.sharedVaultInvites) { + sharedVaultInvites.push(this.sharedVaultInvitesMapper.toProjection(sharedVaultInvite)) + } + + const messages = [] + for (const contact of syncItemsResponse.messages) { + messages.push(this.messageMapper.toProjection(contact)) + } + + const notifications = [] + for (const notification of syncItemsResponse.notifications) { + notifications.push(this.notificationMapper.toProjection(notification)) + } + return { retrieved_items: retrievedItems, saved_items: savedItems, conflicts, sync_token: syncItemsResponse.syncToken, cursor_token: syncItemsResponse.cursorToken, + messages, + shared_vaults: sharedVaults, + shared_vault_invites: sharedVaultInvites, + notifications, } } } diff --git a/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts b/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts index dce22a9ea..3173b89fe 100644 --- a/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts @@ -5,6 +5,7 @@ import { Message } from './Message' export interface MessageRepositoryInterface { findByUuid: (uuid: Uuid) => Promise findByRecipientUuid: (uuid: Uuid) => Promise + findByRecipientUuidUpdatedAfter: (uuid: Uuid, updatedAtTimestamp: number) => Promise findBySenderUuid: (uuid: Uuid) => Promise findByRecipientUuidAndReplaceabilityIdentifier: (dto: { recipientUuid: Uuid diff --git a/packages/syncing-server/src/Domain/Notifications/NotificationRepositoryInterface.ts b/packages/syncing-server/src/Domain/Notifications/NotificationRepositoryInterface.ts index 069413d01..95b3b1848 100644 --- a/packages/syncing-server/src/Domain/Notifications/NotificationRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/Notifications/NotificationRepositoryInterface.ts @@ -1,5 +1,9 @@ +import { Uuid } from '@standardnotes/domain-core' + import { Notification } from './Notification' export interface NotificationRepositoryInterface { save(notification: Notification): Promise + findByUserUuidUpdatedAfter(userUuid: Uuid, lastSyncTime: number): Promise + findByUserUuid(userUuid: Uuid): Promise } diff --git a/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts b/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts index 6de4ed30b..7b3e8e41f 100644 --- a/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts @@ -8,6 +8,7 @@ export interface SharedVaultInviteRepositoryInterface { remove(sharedVaultInvite: SharedVaultInvite): Promise removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise findByUserUuid(userUuid: Uuid): Promise + findByUserUuidUpdatedAfter(userUuid: Uuid, updatedAtTimestamp: number): Promise findBySenderUuid(senderUuid: Uuid): Promise findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise findBySenderUuidAndSharedVaultUuid(dto: { senderUuid: Uuid; sharedVaultUuid: Uuid }): Promise diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts index d7ffb2605..bc77d5f8a 100644 --- a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.spec.ts @@ -9,6 +9,7 @@ describe('GetMessagesSentToUser', () => { beforeEach(() => { messageRepository = {} as jest.Mocked messageRepository.findByRecipientUuid = jest.fn().mockReturnValue([]) + messageRepository.findByRecipientUuidUpdatedAfter = jest.fn().mockReturnValue([]) }) it('should return messages sent to user', async () => { @@ -20,6 +21,16 @@ describe('GetMessagesSentToUser', () => { expect(result.getValue()).toEqual([]) }) + it('should return messages sent to user updated after given time', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + lastSyncTime: 123, + }) + + expect(result.getValue()).toEqual([]) + }) + it('should return error when recipient uuid is invalid', async () => { const useCase = createUseCase() const result = await useCase.execute({ diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts index afb2abe07..390e50db2 100644 --- a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUser.ts @@ -14,8 +14,10 @@ export class GetMessagesSentToUser implements UseCaseInterface { } const recipientUuid = recipientUuidOrError.getValue() - const messages = await this.messageRepository.findByRecipientUuid(recipientUuid) + if (dto.lastSyncTime) { + return Result.ok(await this.messageRepository.findByRecipientUuidUpdatedAfter(recipientUuid, dto.lastSyncTime)) + } - return Result.ok(messages) + return Result.ok(await this.messageRepository.findByRecipientUuid(recipientUuid)) } } diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts index 2547227d3..ab6321d0f 100644 --- a/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetMessagesSentToUser/GetMessagesSentToUserDTO.ts @@ -1,3 +1,4 @@ export interface GetMessagesSentToUserDTO { recipientUuid: string + lastSyncTime?: number } diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.spec.ts new file mode 100644 index 000000000..f2f553788 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.spec.ts @@ -0,0 +1,43 @@ +import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface' +import { GetUserNotifications } from './GetUserNotifications' + +describe('GetUserNotifications', () => { + let notificationRepository: NotificationRepositoryInterface + + const createUseCase = () => new GetUserNotifications(notificationRepository) + + beforeEach(() => { + notificationRepository = {} as jest.Mocked + notificationRepository.findByUserUuid = jest.fn().mockReturnValue([]) + notificationRepository.findByUserUuidUpdatedAfter = jest.fn().mockReturnValue([]) + }) + + it('should return notification for user', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.getValue()).toEqual([]) + }) + + it('should return notifications for user updated after given time', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + lastSyncTime: 123, + }) + + expect(result.getValue()).toEqual([]) + }) + + it('should return error when user uuid is invalid', async () => { + const useCase = createUseCase() + const result = await useCase.execute({ + userUuid: 'invalid', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Given value is not a valid uuid: invalid') + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.ts new file mode 100644 index 000000000..57bad00b3 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications.ts @@ -0,0 +1,22 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { Notification } from '../../../Notifications/Notification' +import { GetUserNotificationsDTO } from './GetUserNotificationsDTO' +import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface' + +export class GetUserNotifications implements UseCaseInterface { + constructor(private notificationRepository: NotificationRepositoryInterface) {} + + async execute(dto: GetUserNotificationsDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + if (dto.lastSyncTime) { + return Result.ok(await this.notificationRepository.findByUserUuidUpdatedAfter(userUuid, dto.lastSyncTime)) + } + + return Result.ok(await this.notificationRepository.findByUserUuid(userUuid)) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotificationsDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotificationsDTO.ts new file mode 100644 index 000000000..d281bfe94 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/GetUserNotifications/GetUserNotificationsDTO.ts @@ -0,0 +1,4 @@ +export interface GetUserNotificationsDTO { + userUuid: string + lastSyncTime?: number +} diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts index 1d54ebf2e..08276dabf 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts @@ -23,6 +23,7 @@ describe('GetSharedVaultInvitesSentToUser', () => { sharedVaultInviteRepository = {} as jest.Mocked sharedVaultInviteRepository.findByUserUuid = jest.fn().mockResolvedValue([invite]) + sharedVaultInviteRepository.findByUserUuidUpdatedAfter = jest.fn().mockResolvedValue([invite]) }) it('should return invites sent to user', async () => { @@ -35,6 +36,17 @@ describe('GetSharedVaultInvitesSentToUser', () => { expect(result.getValue()).toEqual([invite]) }) + it('should return invites sent to user updated after given time', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + lastSyncTime: 123, + }) + + expect(result.getValue()).toEqual([invite]) + }) + it('should return empty array if no invites found', async () => { const useCase = createUseCase() diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts index aa9c0363a..c64fd495b 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts @@ -13,6 +13,10 @@ export class GetSharedVaultInvitesSentToUser implements UseCaseInterface { expect(result.getValue()).toEqual({ items: [item], cursorToken: undefined, + lastSyncTime: null, }) }) @@ -76,6 +77,7 @@ describe('GetItems', () => { expect(result.getValue()).toEqual({ items: [item], cursorToken: 'MjowLjAwMDEyMw==', + lastSyncTime: null, }) }) @@ -93,6 +95,7 @@ describe('GetItems', () => { expect(result.getValue()).toEqual({ items: [item], cursorToken: undefined, + lastSyncTime: 123.00000000000001, }) }) @@ -113,6 +116,7 @@ describe('GetItems', () => { expect(result.getValue()).toEqual({ items: [item], cursorToken: undefined, + lastSyncTime: 123, }) }) @@ -147,6 +151,7 @@ describe('GetItems', () => { expect(result.getValue()).toEqual({ items: [item], cursorToken: undefined, + lastSyncTime: null, }) }) }) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItems.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItems.ts index 35a5cac74..c1e07ba6c 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItems.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItems.ts @@ -65,6 +65,7 @@ export class GetItems implements UseCaseInterface { return Result.ok({ items, cursorToken, + lastSyncTime, }) } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItemsResult.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItemsResult.ts index 9b34f6dae..2c4806535 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItemsResult.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/GetItems/GetItemsResult.ts @@ -3,4 +3,5 @@ import { Item } from '../../../Item/Item' export interface GetItemsResult { items: Item[] cursorToken?: string + lastSyncTime: number | null } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts index 0ca87d60a..0bda2ae60 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts @@ -9,6 +9,10 @@ import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@s import { GetItems } from '../GetItems/GetItems' import { SaveItems } from '../SaveItems/SaveItems' import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface' +import { GetSharedVaults } from '../../SharedVaults/GetSharedVaults/GetSharedVaults' +import { GetMessagesSentToUser } from '../../Messaging/GetMessagesSentToUser/GetMessagesSentToUser' +import { GetUserNotifications } from '../../Messaging/GetUserNotifications/GetUserNotifications' +import { GetSharedVaultInvitesSentToUser } from '../../SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser' describe('SyncItems', () => { let getItemsUseCase: GetItems @@ -18,8 +22,21 @@ describe('SyncItems', () => { let item2: Item let item3: Item let itemHash: ItemHash + let getSharedVaultsUseCase: GetSharedVaults + let getSharedVaultInvitesSentToUserUseCase: GetSharedVaultInvitesSentToUser + let getMessagesSentToUser: GetMessagesSentToUser + let getUserNotifications: GetUserNotifications - const createUseCase = () => new SyncItems(itemRepository, getItemsUseCase, saveItemsUseCase) + const createUseCase = () => + new SyncItems( + itemRepository, + getItemsUseCase, + saveItemsUseCase, + getSharedVaultsUseCase, + getSharedVaultInvitesSentToUserUseCase, + getMessagesSentToUser, + getUserNotifications, + ) beforeEach(() => { item1 = Item.create( @@ -104,6 +121,18 @@ describe('SyncItems', () => { itemRepository = {} as jest.Mocked itemRepository.findAll = jest.fn().mockReturnValue([item3, item1]) + + getSharedVaultsUseCase = {} as jest.Mocked + getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([])) + + getSharedVaultInvitesSentToUserUseCase = {} as jest.Mocked + getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.ok([])) + + getMessagesSentToUser = {} as jest.Mocked + getMessagesSentToUser.execute = jest.fn().mockReturnValue(Result.ok([])) + + getUserNotifications = {} as jest.Mocked + getUserNotifications.execute = jest.fn().mockReturnValue(Result.ok([])) }) it('should sync items', async () => { @@ -126,6 +155,10 @@ describe('SyncItems', () => { retrievedItems: [item1], savedItems: [item2], syncToken: 'qwerty', + sharedVaults: [], + sharedVaultInvites: [], + notifications: [], + messages: [], }) expect(getItemsUseCase.execute).toHaveBeenCalledWith({ @@ -162,6 +195,10 @@ describe('SyncItems', () => { retrievedItems: [item3, item1], savedItems: [item2], syncToken: 'qwerty', + sharedVaults: [], + sharedVaultInvites: [], + notifications: [], + messages: [], }) }) @@ -219,6 +256,10 @@ describe('SyncItems', () => { retrievedItems: [item1], savedItems: [], syncToken: 'qwerty', + sharedVaults: [], + sharedVaultInvites: [], + notifications: [], + messages: [], }) }) @@ -261,4 +302,84 @@ describe('SyncItems', () => { expect(result.isFailed()).toBeTruthy() }) + + it('should return error if get shared vaults fails', async () => { + getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '1-2-3', + itemHashes: [itemHash], + computeIntegrityHash: false, + syncToken: 'foo', + readOnlyAccess: false, + sessionUuid: '2-3-4', + cursorToken: 'bar', + limit: 10, + contentType: 'Note', + apiVersion: ApiVersion.v20200115, + snjsVersion: '1.2.3', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error if get shared vault invites fails', async () => { + getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '1-2-3', + itemHashes: [itemHash], + computeIntegrityHash: false, + syncToken: 'foo', + readOnlyAccess: false, + sessionUuid: '2-3-4', + cursorToken: 'bar', + limit: 10, + contentType: 'Note', + apiVersion: ApiVersion.v20200115, + snjsVersion: '1.2.3', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error if get messages fails', async () => { + getMessagesSentToUser.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '1-2-3', + itemHashes: [itemHash], + computeIntegrityHash: false, + syncToken: 'foo', + readOnlyAccess: false, + sessionUuid: '2-3-4', + cursorToken: 'bar', + limit: 10, + contentType: 'Note', + apiVersion: ApiVersion.v20200115, + snjsVersion: '1.2.3', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error if get user notifications fails', async () => { + getUserNotifications.execute = jest.fn().mockReturnValue(Result.fail('error')) + + const result = await createUseCase().execute({ + userUuid: '1-2-3', + itemHashes: [itemHash], + computeIntegrityHash: false, + syncToken: 'foo', + readOnlyAccess: false, + sessionUuid: '2-3-4', + cursorToken: 'bar', + limit: 10, + contentType: 'Note', + apiVersion: ApiVersion.v20200115, + snjsVersion: '1.2.3', + }) + + expect(result.isFailed()).toBeTruthy() + }) }) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts index 9090a4ae7..65dc8d170 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts @@ -7,12 +7,20 @@ import { SyncItemsResponse } from './SyncItemsResponse' import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface' import { GetItems } from '../GetItems/GetItems' import { SaveItems } from '../SaveItems/SaveItems' +import { GetSharedVaults } from '../../SharedVaults/GetSharedVaults/GetSharedVaults' +import { GetSharedVaultInvitesSentToUser } from '../../SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser' +import { GetMessagesSentToUser } from '../../Messaging/GetMessagesSentToUser/GetMessagesSentToUser' +import { GetUserNotifications } from '../../Messaging/GetUserNotifications/GetUserNotifications' export class SyncItems implements UseCaseInterface { constructor( private itemRepository: ItemRepositoryInterface, private getItemsUseCase: GetItems, private saveItemsUseCase: SaveItems, + private getSharedVaultsUseCase: GetSharedVaults, + private getSharedVaultInvitesSentToUserUseCase: GetSharedVaultInvitesSentToUser, + private getMessagesSentToUser: GetMessagesSentToUser, + private getUserNotifications: GetUserNotifications, ) {} async execute(dto: SyncItemsDTO): Promise> { @@ -45,12 +53,52 @@ export class SyncItems implements UseCaseInterface { retrievedItems = await this.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems) } + const sharedVaultsOrError = await this.getSharedVaultsUseCase.execute({ + userUuid: dto.userUuid, + lastSyncTime: getItemsResult.lastSyncTime ?? undefined, + }) + if (sharedVaultsOrError.isFailed()) { + return Result.fail(sharedVaultsOrError.getError()) + } + const sharedVaults = sharedVaultsOrError.getValue() + + const sharedVaultInvitesOrError = await this.getSharedVaultInvitesSentToUserUseCase.execute({ + userUuid: dto.userUuid, + lastSyncTime: getItemsResult.lastSyncTime ?? undefined, + }) + if (sharedVaultInvitesOrError.isFailed()) { + return Result.fail(sharedVaultInvitesOrError.getError()) + } + const sharedVaultInvites = sharedVaultInvitesOrError.getValue() + + const messagesOrError = await this.getMessagesSentToUser.execute({ + recipientUuid: dto.userUuid, + lastSyncTime: getItemsResult.lastSyncTime ?? undefined, + }) + if (messagesOrError.isFailed()) { + return Result.fail(messagesOrError.getError()) + } + const messages = messagesOrError.getValue() + + const notificationsOrError = await this.getUserNotifications.execute({ + userUuid: dto.userUuid, + lastSyncTime: getItemsResult.lastSyncTime ?? undefined, + }) + if (notificationsOrError.isFailed()) { + return Result.fail(notificationsOrError.getError()) + } + const notifications = notificationsOrError.getValue() + const syncResponse: SyncItemsResponse = { retrievedItems, syncToken: saveItemsResult.syncToken, savedItems: saveItemsResult.savedItems, conflicts: saveItemsResult.conflicts, cursorToken: getItemsResult.cursorToken, + sharedVaultInvites, + sharedVaults, + messages, + notifications, } return Result.ok(syncResponse) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItemsResponse.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItemsResponse.ts index 108a73a03..ac6d020fd 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItemsResponse.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItemsResponse.ts @@ -1,10 +1,18 @@ import { Item } from '../../../Item/Item' import { ItemConflict } from '../../../Item/ItemConflict' +import { Message } from '../../../Message/Message' +import { Notification } from '../../../Notifications/Notification' +import { SharedVault } from '../../../SharedVault/SharedVault' +import { SharedVaultInvite } from '../../../SharedVault/User/Invite/SharedVaultInvite' export type SyncItemsResponse = { retrievedItems: Array savedItems: Array conflicts: Array syncToken: string + sharedVaults: SharedVault[] + sharedVaultInvites: SharedVaultInvite[] + messages: Message[] + notifications: Notification[] cursorToken?: string } diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts index 3d6e47da6..404fe54ab 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts @@ -62,7 +62,7 @@ export class HomeServerSharedVaultInvitesController extends BaseHttpController { const result = await this.inviteUserToSharedVaultUseCase.execute({ sharedVaultUuid: request.params.sharedVaultUuid, senderUuid: response.locals.user.uuid, - recipientUuid: request.body.recipient_uid, + recipientUuid: request.body.recipient_uuid, encryptedMessage: request.body.encrypted_message, permission: request.body.permission, }) diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts index 10d8ab5c1..a8c79a0b2 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts @@ -11,6 +11,20 @@ export class TypeORMMessageRepository implements MessageRepositoryInterface { private mapper: MapperInterface, ) {} + async findByRecipientUuidUpdatedAfter(uuid: Uuid, updatedAtTimestamp: number): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('message') + .where('message.recipient_uuid = :recipientUuid', { + recipientUuid: uuid.value, + }) + .andWhere('message.updated_at_timestamp > :updatedAtTimestamp', { + updatedAtTimestamp, + }) + .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) + } + async findByRecipientUuid(uuid: Uuid): Promise { const persistence = await this.ormRepository .createQueryBuilder('message') diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMNotificationRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMNotificationRepository.ts index f221d6bf4..517e9cf87 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMNotificationRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMNotificationRepository.ts @@ -1,5 +1,5 @@ import { Repository } from 'typeorm' -import { MapperInterface } from '@standardnotes/domain-core' +import { MapperInterface, Uuid } from '@standardnotes/domain-core' import { NotificationRepositoryInterface } from '../../Domain/Notifications/NotificationRepositoryInterface' import { TypeORMNotification } from './TypeORMNotification' @@ -16,4 +16,29 @@ export class TypeORMNotificationRepository implements NotificationRepositoryInte await this.ormRepository.save(persistence) } + + async findByUserUuidUpdatedAfter(uuid: Uuid, updatedAtTimestamp: number): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('notification') + .where('notification.user_uuid = :userUuid', { + userUuid: uuid.value, + }) + .andWhere('notification.updated_at_timestamp > :updatedAtTimestamp', { + updatedAtTimestamp, + }) + .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) + } + + async findByUserUuid(uuid: Uuid): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('notification') + .where('notification.user_uuid = :userUuid', { + userUuid: uuid.value, + }) + .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) + } } diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts index d394f8062..b00946117 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts @@ -11,6 +11,20 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo private mapper: MapperInterface, ) {} + async findByUserUuidUpdatedAfter(userUuid: Uuid, updatedAtTimestamp: number): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('shared_vault_invite') + .where('shared_vault_invite.user_uuid = :userUuid', { + userUuid: userUuid.value, + }) + .andWhere('shared_vault_invite.updated_at_timestamp > :updatedAtTimestamp', { + updatedAtTimestamp, + }) + .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) + } + async findBySenderUuidAndSharedVaultUuid(dto: { senderUuid: Uuid sharedVaultUuid: Uuid diff --git a/packages/syncing-server/src/Mapping/Http/NotificationHttpMapper.ts b/packages/syncing-server/src/Mapping/Http/NotificationHttpMapper.ts new file mode 100644 index 000000000..2f9ea98f4 --- /dev/null +++ b/packages/syncing-server/src/Mapping/Http/NotificationHttpMapper.ts @@ -0,0 +1,21 @@ +import { MapperInterface } from '@standardnotes/domain-core' + +import { Notification } from '../../Domain/Notifications/Notification' +import { NotificationHttpRepresentation } from './NotificationHttpRepresentation' + +export class NotificationHttpMapper implements MapperInterface { + toDomain(_projection: NotificationHttpRepresentation): Notification { + throw new Error('Mapping from http representation to domain is not implemented.') + } + + toProjection(domain: Notification): NotificationHttpRepresentation { + return { + uuid: domain.id.toString(), + user_uuid: domain.props.userUuid.value, + type: domain.props.type.value, + payload: domain.props.payload, + created_at_timestamp: domain.props.timestamps.createdAt, + updated_at_timestamp: domain.props.timestamps.updatedAt, + } + } +} diff --git a/packages/syncing-server/src/Mapping/Http/NotificationHttpRepresentation.ts b/packages/syncing-server/src/Mapping/Http/NotificationHttpRepresentation.ts new file mode 100644 index 000000000..661ede7fe --- /dev/null +++ b/packages/syncing-server/src/Mapping/Http/NotificationHttpRepresentation.ts @@ -0,0 +1,8 @@ +export interface NotificationHttpRepresentation { + uuid: string + user_uuid: string + type: string + payload: string + created_at_timestamp: number + updated_at_timestamp: number +} diff --git a/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts b/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts index 2a0b22870..2154fa9d9 100644 --- a/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts +++ b/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts @@ -17,7 +17,7 @@ export class SharedVaultInviteHttpMapper user_uuid: domain.props.userUuid.value, sender_uuid: domain.props.senderUuid.value, encrypted_message: domain.props.encryptedMessage, - permissions: domain.props.permission.value, + permission: domain.props.permission.value, created_at_timestamp: domain.props.timestamps.createdAt, updated_at_timestamp: domain.props.timestamps.updatedAt, } diff --git a/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts b/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts index 509d9e568..6194edaf6 100644 --- a/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts +++ b/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts @@ -4,7 +4,7 @@ export interface SharedVaultInviteHttpRepresentation { user_uuid: string sender_uuid: string encrypted_message: string - permissions: string + permission: string created_at_timestamp: number updated_at_timestamp: number }