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
This commit is contained in:
Karol Sójko 2023-07-20 11:52:45 +02:00 committed by GitHub
parent f714aaa0e9
commit efa4d7fc60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 545 additions and 113 deletions

View file

@ -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,
),

View file

@ -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

View file

@ -1,16 +0,0 @@
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`')
}
}

View file

@ -1,16 +0,0 @@
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',
)
}
}

View file

@ -1,17 +0,0 @@
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"')
}
}

View file

@ -1,17 +0,0 @@
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") ')
}
}

View file

@ -1,16 +0,0 @@
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`')
}
}

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1689671563304 implements MigrationInterface {
name = 'AddNotifications1689671563304'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
await queryRunner.query('DROP TABLE `notifications`')
}
}

View file

@ -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<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)',
'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") ')
}

View file

@ -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<MapperInterface<Message, MessageHttpRepresentation>>(TYPES.Sync_MessageHttpMapper)
.toConstantValue(new MessageHttpMapper())
container
.bind<MapperInterface<Notification, NotificationHttpRepresentation>>(TYPES.Sync_NotificationHttpMapper)
.toConstantValue(new NotificationHttpMapper())
// ORM
container
@ -550,6 +556,24 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_Logger),
),
)
container
.bind<GetUserNotifications>(TYPES.Sync_GetUserNotifications)
.toConstantValue(new GetUserNotifications(container.get(TYPES.Sync_NotificationRepository)))
container
.bind<GetSharedVaults>(TYPES.Sync_GetSharedVaults)
.toConstantValue(
new GetSharedVaults(
container.get(TYPES.Sync_SharedVaultUserRepository),
container.get(TYPES.Sync_SharedVaultRepository),
),
)
container
.bind<GetSharedVaultInvitesSentToUser>(TYPES.Sync_GetSharedVaultInvitesSentToUser)
.toConstantValue(new GetSharedVaultInvitesSentToUser(container.get(TYPES.Sync_SharedVaultInviteRepository)))
container
.bind<GetMessagesSentToUser>(TYPES.Sync_GetMessagesSentToUser)
.toConstantValue(new GetMessagesSentToUser(container.get(TYPES.Sync_MessageRepository)))
container
.bind<SyncItems>(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<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
@ -621,9 +649,6 @@ export class ContainerConfigLoader {
container
.bind<GetSharedVaultInvitesSentByUser>(TYPES.Sync_GetSharedVaultInvitesSentByUser)
.toConstantValue(new GetSharedVaultInvitesSentByUser(container.get(TYPES.Sync_SharedVaultInviteRepository)))
container
.bind<GetSharedVaultInvitesSentToUser>(TYPES.Sync_GetSharedVaultInvitesSentToUser)
.toConstantValue(new GetSharedVaultInvitesSentToUser(container.get(TYPES.Sync_SharedVaultInviteRepository)))
container
.bind<GetSharedVaultUsers>(TYPES.Sync_GetSharedVaultUsers)
.toConstantValue(
@ -646,14 +671,6 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_AddNotificationForUser),
),
)
container
.bind<GetSharedVaults>(TYPES.Sync_GetSharedVaults)
.toConstantValue(
new GetSharedVaults(
container.get(TYPES.Sync_SharedVaultUserRepository),
container.get(TYPES.Sync_SharedVaultRepository),
),
)
container
.bind<CreateSharedVault>(TYPES.Sync_CreateSharedVault)
.toConstantValue(
@ -683,9 +700,6 @@ export class ContainerConfigLoader {
container.get(TYPES.Sync_VALET_TOKEN_TTL),
),
)
container
.bind<GetMessagesSentToUser>(TYPES.Sync_GetMessagesSentToUser)
.toConstantValue(new GetMessagesSentToUser(container.get(TYPES.Sync_MessageRepository)))
container
.bind<GetMessagesSentByUser>(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

View file

@ -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'),

View file

@ -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<ItemHttpRepresentation>
saved_items: Array<SavedItemHttpRepresentation>
conflicts: Array<ItemConflictHttpRepresentation>
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[]
}

View file

@ -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: [],

View file

@ -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<Item, ItemHttpRepresentation>
@ -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<SharedVault, SharedVaultHttpRepresentation>
let sharedVaultInvitesMapper: MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>
let messageMapper: MapperInterface<Message, MessageHttpRepresentation>
let notificationMapper: MapperInterface<Notification, NotificationHttpRepresentation>
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<Item>
itemConflict = {} as jest.Mocked<ItemConflict>
sharedVaultMapper = {} as jest.Mocked<MapperInterface<SharedVault, SharedVaultHttpRepresentation>>
sharedVaultMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked<SharedVaultHttpRepresentation>)
sharedVaultInvitesMapper = {} as jest.Mocked<
MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>
>
sharedVaultInvitesMapper.toProjection = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedVaultInviteHttpRepresentation>)
messageMapper = {} as jest.Mocked<MapperInterface<Message, MessageHttpRepresentation>>
messageMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked<MessageHttpRepresentation>)
notificationMapper = {} as jest.Mocked<MapperInterface<Notification, NotificationHttpRepresentation>>
notificationMapper.toProjection = jest.fn().mockReturnValue({} as jest.Mocked<NotificationHttpRepresentation>)
sharedVault = {} as jest.Mocked<SharedVault>
sharedVaultInvite = {} as jest.Mocked<SharedVaultInvite>
message = {} as jest.Mocked<Message>
notification = {} as jest.Mocked<Notification>
})
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<SharedVaultHttpRepresentation>],
shared_vault_invites: [{} as jest.Mocked<SharedVaultInviteHttpRepresentation>],
messages: [{} as jest.Mocked<MessageHttpRepresentation>],
notifications: [{} as jest.Mocked<NotificationHttpRepresentation>],
})
})
})

View file

@ -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<Item, ItemHttpRepresentation>,
private itemConflictMapper: MapperInterface<ItemConflict, ItemConflictHttpRepresentation>,
private savedItemMapper: MapperInterface<Item, SavedItemHttpRepresentation>,
private sharedVaultMapper: MapperInterface<SharedVault, SharedVaultHttpRepresentation>,
private sharedVaultInvitesMapper: MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>,
private messageMapper: MapperInterface<Message, MessageHttpRepresentation>,
private notificationMapper: MapperInterface<Notification, NotificationHttpRepresentation>,
) {}
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {
@ -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,
}
}
}

View file

@ -5,6 +5,7 @@ import { Message } from './Message'
export interface MessageRepositoryInterface {
findByUuid: (uuid: Uuid) => Promise<Message | null>
findByRecipientUuid: (uuid: Uuid) => Promise<Message[]>
findByRecipientUuidUpdatedAfter: (uuid: Uuid, updatedAtTimestamp: number) => Promise<Message[]>
findBySenderUuid: (uuid: Uuid) => Promise<Message[]>
findByRecipientUuidAndReplaceabilityIdentifier: (dto: {
recipientUuid: Uuid

View file

@ -1,5 +1,9 @@
import { Uuid } from '@standardnotes/domain-core'
import { Notification } from './Notification'
export interface NotificationRepositoryInterface {
save(notification: Notification): Promise<void>
findByUserUuidUpdatedAfter(userUuid: Uuid, lastSyncTime: number): Promise<Notification[]>
findByUserUuid(userUuid: Uuid): Promise<Notification[]>
}

View file

@ -8,6 +8,7 @@ export interface SharedVaultInviteRepositoryInterface {
remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
findByUserUuid(userUuid: Uuid): Promise<SharedVaultInvite[]>
findByUserUuidUpdatedAfter(userUuid: Uuid, updatedAtTimestamp: number): Promise<SharedVaultInvite[]>
findBySenderUuid(senderUuid: Uuid): Promise<SharedVaultInvite[]>
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite | null>
findBySenderUuidAndSharedVaultUuid(dto: { senderUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite[]>

View file

@ -9,6 +9,7 @@ describe('GetMessagesSentToUser', () => {
beforeEach(() => {
messageRepository = {} as jest.Mocked<MessageRepositoryInterface>
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({

View file

@ -14,8 +14,10 @@ export class GetMessagesSentToUser implements UseCaseInterface<Message[]> {
}
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))
}
}

View file

@ -1,3 +1,4 @@
export interface GetMessagesSentToUserDTO {
recipientUuid: string
lastSyncTime?: number
}

View file

@ -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<NotificationRepositoryInterface>
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')
})
})

View file

@ -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<Notification[]> {
constructor(private notificationRepository: NotificationRepositoryInterface) {}
async execute(dto: GetUserNotificationsDTO): Promise<Result<Notification[]>> {
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))
}
}

View file

@ -0,0 +1,4 @@
export interface GetUserNotificationsDTO {
userUuid: string
lastSyncTime?: number
}

View file

@ -23,6 +23,7 @@ describe('GetSharedVaultInvitesSentToUser', () => {
sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
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()

View file

@ -13,6 +13,10 @@ export class GetSharedVaultInvitesSentToUser implements UseCaseInterface<SharedV
}
const userUuid = userUuidOrError.getValue()
if (dto.lastSyncTime) {
return Result.ok(await this.sharedVaultInviteRepository.findByUserUuidUpdatedAfter(userUuid, dto.lastSyncTime))
}
return Result.ok(await this.sharedVaultInviteRepository.findByUserUuid(userUuid))
}
}

View file

@ -1,3 +1,4 @@
export interface GetSharedVaultInvitesSentToUserDTO {
userUuid: string
lastSyncTime?: number
}

View file

@ -57,6 +57,7 @@ describe('GetItems', () => {
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,
})
})
})

View file

@ -65,6 +65,7 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
return Result.ok({
items,
cursorToken,
lastSyncTime,
})
}

View file

@ -3,4 +3,5 @@ import { Item } from '../../../Item/Item'
export interface GetItemsResult {
items: Item[]
cursorToken?: string
lastSyncTime: number | null
}

View file

@ -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<ItemRepositoryInterface>
itemRepository.findAll = jest.fn().mockReturnValue([item3, item1])
getSharedVaultsUseCase = {} as jest.Mocked<GetSharedVaults>
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
getSharedVaultInvitesSentToUserUseCase = {} as jest.Mocked<GetSharedVaultInvitesSentToUser>
getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
getMessagesSentToUser = {} as jest.Mocked<GetMessagesSentToUser>
getMessagesSentToUser.execute = jest.fn().mockReturnValue(Result.ok([]))
getUserNotifications = {} as jest.Mocked<GetUserNotifications>
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()
})
})

View file

@ -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<SyncItemsResponse> {
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<Result<SyncItemsResponse>> {
@ -45,12 +53,52 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
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)

View file

@ -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<Item>
savedItems: Array<Item>
conflicts: Array<ItemConflict>
syncToken: string
sharedVaults: SharedVault[]
sharedVaultInvites: SharedVaultInvite[]
messages: Message[]
notifications: Notification[]
cursorToken?: string
}

View file

@ -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,
})

View file

@ -11,6 +11,20 @@ export class TypeORMMessageRepository implements MessageRepositoryInterface {
private mapper: MapperInterface<Message, TypeORMMessage>,
) {}
async findByRecipientUuidUpdatedAfter(uuid: Uuid, updatedAtTimestamp: number): Promise<Message[]> {
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<Message[]> {
const persistence = await this.ormRepository
.createQueryBuilder('message')

View file

@ -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<Notification[]> {
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<Notification[]> {
const persistence = await this.ormRepository
.createQueryBuilder('notification')
.where('notification.user_uuid = :userUuid', {
userUuid: uuid.value,
})
.getMany()
return persistence.map((p) => this.mapper.toDomain(p))
}
}

View file

@ -11,6 +11,20 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
private mapper: MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>,
) {}
async findByUserUuidUpdatedAfter(userUuid: Uuid, updatedAtTimestamp: number): Promise<SharedVaultInvite[]> {
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

View file

@ -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<Notification, NotificationHttpRepresentation> {
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,
}
}
}

View file

@ -0,0 +1,8 @@
export interface NotificationHttpRepresentation {
uuid: string
user_uuid: string
type: string
payload: string
created_at_timestamp: number
updated_at_timestamp: number
}

View file

@ -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,
}

View file

@ -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
}