feat(syncing-server): associating existing items with key systems and shared vaults (#661)

* feat(syncing-server): associating existing items with key systems and shared vaults

* fix(syncing-server): find item query

* feat(syncing-server): add persistence of shared vaults with users and invites
This commit is contained in:
Karol Sójko 2023-07-18 13:21:30 +02:00 committed by GitHub
parent b32f851a90
commit 3b804e2321
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 488 additions and 20 deletions

View file

@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddSharedVaultsWithUsersAndInvites1689677728282 implements MigrationInterface {
name = 'AddSharedVaultsWithUsersAndInvites1689677728282'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `shared_vaults` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `file_upload_bytes_used` int NOT NULL, `file_upload_bytes_limit` int NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `user_uuid_on_shared_vaults` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await queryRunner.query(
'CREATE TABLE `shared_vault_users` (`uuid` varchar(36) NOT NULL, `shared_vault_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `permission` varchar(24) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `shared_vault_uuid_on_shared_vault_users` (`shared_vault_uuid`), INDEX `user_uuid_on_shared_vault_users` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await queryRunner.query(
'CREATE TABLE `shared_vault_invites` (`uuid` varchar(36) NOT NULL, `shared_vault_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `sender_uuid` varchar(36) NOT NULL, `encrypted_message` text NOT NULL, `permission` varchar(24) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `shared_vault_uuid_on_shared_vault_invites` (`shared_vault_uuid`), INDEX `user_uuid_on_shared_vault_invites` (`user_uuid`), INDEX `sender_uuid_on_shared_vault_invites` (`sender_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `sender_uuid_on_shared_vault_invites` ON `shared_vault_invites`')
await queryRunner.query('DROP INDEX `user_uuid_on_shared_vault_invites` ON `shared_vault_invites`')
await queryRunner.query('DROP INDEX `shared_vault_uuid_on_shared_vault_invites` ON `shared_vault_invites`')
await queryRunner.query('DROP TABLE `shared_vault_invites`')
await queryRunner.query('DROP INDEX `user_uuid_on_shared_vault_users` ON `shared_vault_users`')
await queryRunner.query('DROP INDEX `shared_vault_uuid_on_shared_vault_users` ON `shared_vault_users`')
await queryRunner.query('DROP TABLE `shared_vault_users`')
await queryRunner.query('DROP INDEX `user_uuid_on_shared_vaults` ON `shared_vaults`')
await queryRunner.query('DROP TABLE `shared_vaults`')
}
}

View file

@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddSharedVaultsWithUsersAndInvites1689677867175 implements MigrationInterface {
name = 'AddSharedVaultsWithUsersAndInvites1689677867175'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "shared_vaults" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "file_upload_bytes_used" integer NOT NULL, "file_upload_bytes_limit" integer NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query('CREATE INDEX "user_uuid_on_shared_vaults" ON "shared_vaults" ("user_uuid") ')
await queryRunner.query(
'CREATE TABLE "shared_vault_users" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36) NOT NULL, "permission" varchar(24) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query(
'CREATE INDEX "shared_vault_uuid_on_shared_vault_users" ON "shared_vault_users" ("shared_vault_uuid") ',
)
await queryRunner.query('CREATE INDEX "user_uuid_on_shared_vault_users" ON "shared_vault_users" ("user_uuid") ')
await queryRunner.query(
'CREATE TABLE "shared_vault_invites" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36) NOT NULL, "sender_uuid" varchar(36) NOT NULL, "encrypted_message" text NOT NULL, "permission" varchar(24) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
)
await queryRunner.query(
'CREATE INDEX "shared_vault_uuid_on_shared_vault_invites" ON "shared_vault_invites" ("shared_vault_uuid") ',
)
await queryRunner.query('CREATE INDEX "user_uuid_on_shared_vault_invites" ON "shared_vault_invites" ("user_uuid") ')
await queryRunner.query(
'CREATE INDEX "sender_uuid_on_shared_vault_invites" ON "shared_vault_invites" ("sender_uuid") ',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "sender_uuid_on_shared_vault_invites"')
await queryRunner.query('DROP INDEX "user_uuid_on_shared_vault_invites"')
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_shared_vault_invites"')
await queryRunner.query('DROP TABLE "shared_vault_invites"')
await queryRunner.query('DROP INDEX "user_uuid_on_shared_vault_users"')
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_shared_vault_users"')
await queryRunner.query('DROP TABLE "shared_vault_users"')
await queryRunner.query('DROP INDEX "user_uuid_on_shared_vaults"')
await queryRunner.query('DROP TABLE "shared_vaults"')
}
}

View file

@ -6,6 +6,9 @@ import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
import { TypeORMSharedVault } from '../Infra/TypeORM/TypeORMSharedVault'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
import { TypeORMSharedVaultInvite } from '../Infra/TypeORM/TypeORMSharedVaultInvite'
export class AppDataSource {
private _dataSource: DataSource | undefined
@ -35,7 +38,15 @@ export class AppDataSource {
const commonDataSourceOptions = {
maxQueryExecutionTime,
entities: [TypeORMItem, TypeORMNotification, TypeORMSharedVaultAssociation, TypeORMKeySystemAssociation],
entities: [
TypeORMItem,
TypeORMNotification,
TypeORMSharedVaultAssociation,
TypeORMKeySystemAssociation,
TypeORMSharedVault,
TypeORMSharedVaultUser,
TypeORMSharedVaultInvite,
],
migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,
logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',

View file

@ -53,7 +53,7 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
event = {} as jest.Mocked<ItemRevisionCreationRequestedEvent>
event.createdAt = new Date(1)
event.payload = {
itemUuid: '2-3-4',
itemUuid: '00000000-0000-0000-0000-000000000000',
}
event.meta = {
correlation: {
@ -96,4 +96,13 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
})
it('should not create a revision if the item uuid is invalid', async () => {
event.payload.itemUuid = 'invalid-uuid'
await createHandler().handle(event)
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
})
})

View file

@ -3,6 +3,7 @@ import {
DomainEventHandlerInterface,
DomainEventPublisherInterface,
} from '@standardnotes/domain-events'
import { Uuid } from '@standardnotes/domain-core'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
@ -17,7 +18,13 @@ export class ItemRevisionCreationRequestedEventHandler implements DomainEventHan
) {}
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
const item = await this.itemRepository.findByUuid(event.payload.itemUuid)
const itemUuidOrError = Uuid.create(event.payload.itemUuid)
if (itemUuidOrError.isFailed()) {
return
}
const itemUuid = itemUuidOrError.getValue()
const item = await this.itemRepository.findByUuid(itemUuid)
if (item === null) {
return
}

View file

@ -1,6 +1,8 @@
import { Uuid } from '@standardnotes/domain-core'
import { ReadStream } from 'fs'
import { Item } from './Item'
import { ItemQuery } from './ItemQuery'
import { ReadStream } from 'fs'
import { ExtendedIntegrityPayload } from './ExtendedIntegrityPayload'
export interface ItemRepositoryInterface {
@ -15,7 +17,7 @@ export interface ItemRepositoryInterface {
findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>>
findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]>
findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null>
findByUuid(uuid: string): Promise<Item | null>
findByUuid(uuid: Uuid): Promise<Item | null>
remove(item: Item): Promise<void>
save(item: Item): Promise<void>
markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void>

View file

@ -1,6 +1,9 @@
import { Uuid } from '@standardnotes/domain-core'
import { KeySystemAssociation } from './KeySystemAssociation'
export interface KeySystemAssociationRepositoryInterface {
save(keySystem: KeySystemAssociation): Promise<void>
remove(keySystem: KeySystemAssociation): Promise<void>
findByItemUuid(itemUuid: Uuid): Promise<KeySystemAssociation | null>
}

View file

@ -1,6 +1,9 @@
import { Uuid } from '@standardnotes/domain-core'
import { SharedVaultAssociation } from './SharedVaultAssociation'
export interface SharedVaultAssociationRepositoryInterface {
save(sharedVaultAssociation: SharedVaultAssociation): Promise<void>
remove(sharedVaultAssociation: SharedVaultAssociation): Promise<void>
findByItemUuid(itemUuid: Uuid): Promise<SharedVaultAssociation | null>
}

View file

@ -56,7 +56,7 @@ describe('SaveItems', () => {
logger.error = jest.fn()
itemHash1 = ItemHash.create({
uuid: 'item-uuid',
uuid: '00000000-0000-0000-0000-000000000000',
user_uuid: 'user-uuid',
content: 'content',
content_type: ContentType.TYPES.Note,
@ -197,7 +197,7 @@ describe('SaveItems', () => {
const result = await useCase.execute({
itemHashes: [itemHash1],
userUuid: 'user-uuid',
userUuid: '00000000-0000-0000-0000-000000000000',
apiVersion: '1',
readOnlyAccess: false,
sessionUuid: 'session-uuid',
@ -208,6 +208,7 @@ describe('SaveItems', () => {
itemHash: itemHash1,
existingItem: savedItem,
sessionUuid: 'session-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
})
@ -234,6 +235,29 @@ describe('SaveItems', () => {
])
})
it('should mark items as conflict if the item uuid is invalid', async () => {
const useCase = createUseCase()
itemRepository.findByUuid = jest.fn().mockResolvedValue(savedItem)
updateExistingItem.execute = jest.fn().mockResolvedValue(Result.fail('error'))
const result = await useCase.execute({
itemHashes: [ItemHash.create({ ...itemHash1.props, uuid: 'invalid-uuid' }).getValue()],
userUuid: 'user-uuid',
apiVersion: '1',
readOnlyAccess: false,
sessionUuid: 'session-uuid',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().conflicts).toEqual([
{
unsavedItem: ItemHash.create({ ...itemHash1.props, uuid: 'invalid-uuid' }).getValue(),
type: 'uuid_conflict',
},
])
})
it('should calculate the sync token based on existing and new items saved', async () => {
const useCase = createUseCase()
@ -260,8 +284,8 @@ describe('SaveItems', () => {
const result = await useCase.execute({
itemHashes: [
itemHash1,
ItemHash.create({ ...itemHash1.props, uuid: 'item-uuid-2' }).getValue(),
ItemHash.create({ ...itemHash1.props, uuid: 'item-uuid-2' }).getValue(),
ItemHash.create({ ...itemHash1.props, uuid: '00000000-0000-0000-0000-000000000002' }).getValue(),
ItemHash.create({ ...itemHash1.props, uuid: '00000000-0000-0000-0000-000000000003' }).getValue(),
],
userUuid: 'user-uuid',
apiVersion: '2',

View file

@ -1,4 +1,4 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SaveItemsResult } from './SaveItemsResult'
import { SaveItemsDTO } from './SaveItemsDTO'
@ -40,7 +40,18 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
continue
}
const existingItem = await this.itemRepository.findByUuid(itemHash.props.uuid)
const itemUuidOrError = Uuid.create(itemHash.props.uuid)
if (itemUuidOrError.isFailed()) {
conflicts.push({
unsavedItem: itemHash,
type: ConflictType.UuidConflict,
})
continue
}
const itemUuid = itemUuidOrError.getValue()
const existingItem = await this.itemRepository.findByUuid(itemUuid)
const processingResult = await this.itemSaveValidator.validate({
userUuid: dto.userUuid,
apiVersion: dto.apiVersion,
@ -63,6 +74,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
existingItem,
itemHash,
sessionUuid: dto.sessionUuid,
performingUserUuid: dto.userUuid,
})
if (udpatedItemOrError.isFailed()) {
this.logger.error(

View file

@ -6,6 +6,8 @@ import { ItemHash } from '../../../Item/ItemHash'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { UpdateExistingItem } from './UpdateExistingItem'
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
describe('UpdateExistingItem', () => {
let itemRepository: ItemRepositoryInterface
@ -87,6 +89,7 @@ describe('UpdateExistingItem', () => {
existingItem: item1,
itemHash: itemHash1,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
@ -100,6 +103,7 @@ describe('UpdateExistingItem', () => {
existingItem: item1,
itemHash: itemHash1,
sessionUuid: 'invalid-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
@ -115,6 +119,7 @@ describe('UpdateExistingItem', () => {
content_type: 'invalid',
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
@ -130,6 +135,7 @@ describe('UpdateExistingItem', () => {
deleted: true,
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
@ -152,6 +158,7 @@ describe('UpdateExistingItem', () => {
duplicate_of: '00000000-0000-0000-0000-000000000001',
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
@ -169,6 +176,7 @@ describe('UpdateExistingItem', () => {
duplicate_of: 'invalid-uuid',
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
@ -185,6 +193,7 @@ describe('UpdateExistingItem', () => {
created_at_timestamp: 123,
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
@ -202,6 +211,7 @@ describe('UpdateExistingItem', () => {
created_at_timestamp: undefined,
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
@ -223,6 +233,7 @@ describe('UpdateExistingItem', () => {
updated_at_timestamp: 123,
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
@ -246,9 +257,203 @@ describe('UpdateExistingItem', () => {
updated_at_timestamp: 123,
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()
})
it('should return error if performing user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
existingItem: item1,
itemHash: itemHash1,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: 'invalid-uuid',
})
expect(result.isFailed()).toBeTruthy()
})
describe('when item is associated to a shared vault', () => {
it('should add a shared vault association if item hash represents a shared vault item and the existing item is not already associated to the shared vault', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
}).getValue()
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
expect(item1.props.sharedVaultAssociation?.props.sharedVaultUuid.value).toBe(
'00000000-0000-0000-0000-000000000000',
)
})
it('should not add a shared vault association if item hash represents a shared vault item and the existing item is already associated to the shared vault', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
}).getValue()
item1.props.sharedVaultAssociation = SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const idBefore = item1.props.sharedVaultAssociation?.id.toString()
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
expect(item1.props.sharedVaultAssociation.id.toString()).toEqual(idBefore)
})
it('should return error if shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
shared_vault_uuid: 'invalid-uuid',
}).getValue()
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if shared vault association could not be created', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
}).getValue()
const mock = jest.spyOn(SharedVaultAssociation, 'create')
mock.mockImplementation(() => {
return Result.fail('Oops')
})
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()
})
})
describe('when item is associated to a key system', () => {
it('should add a key system association if item hash has a dedicated key system and the existing item is not already associated to the key system', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
key_system_identifier: '00000000-0000-0000-0000-000000000000',
}).getValue()
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.keySystemAssociation).not.toBeUndefined()
expect(item1.props.keySystemAssociation?.props.keySystemUuid.value).toBe('00000000-0000-0000-0000-000000000000')
})
it('should not add a key system association if item hash has a dedicated key system and the existing item is already associated to the key system', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
key_system_identifier: '00000000-0000-0000-0000-000000000000',
}).getValue()
item1.props.keySystemAssociation = KeySystemAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
keySystemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const idBefore = item1.props.keySystemAssociation?.id.toString()
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.keySystemAssociation).not.toBeUndefined()
expect(item1.props.keySystemAssociation.id.toString()).toEqual(idBefore)
})
it('should return error if key system uuid is invalid', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
key_system_identifier: 'invalid-uuid',
}).getValue()
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if key system association could not be created', async () => {
const useCase = createUseCase()
const itemHash = ItemHash.create({
...itemHash1.props,
key_system_identifier: '00000000-0000-0000-0000-000000000000',
}).getValue()
const mock = jest.spyOn(KeySystemAssociation, 'create')
mock.mockImplementation(() => {
return Result.fail('Oops')
})
const result = await useCase.execute({
existingItem: item1,
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()
})
})
})

View file

@ -6,6 +6,9 @@ import { Item } from '../../../Item/Item'
import { UpdateExistingItemDTO } from './UpdateExistingItemDTO'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
import { ItemHash } from '../../../Item/ItemHash'
export class UpdateExistingItem implements UseCaseInterface<Item> {
constructor(
@ -27,6 +30,12 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
}
dto.existingItem.props.updatedWithSession = sessionUuid
const userUuidOrError = Uuid.create(dto.performingUserUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
if (dto.itemHash.props.content) {
dto.existingItem.props.content = dto.itemHash.props.content
}
@ -96,6 +105,57 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
if (
dto.itemHash.representsASharedVaultItem() &&
!this.itemIsAlreadyAssociatedWithTheSharedVault(dto.existingItem, dto.itemHash)
) {
const sharedVaultUuidOrError = Uuid.create(dto.itemHash.props.shared_vault_uuid as string)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const sharedVaultAssociationOrError = SharedVaultAssociation.create({
lastEditedBy: userUuid,
sharedVaultUuid,
timestamps: Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue(),
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
})
if (sharedVaultAssociationOrError.isFailed()) {
return Result.fail(sharedVaultAssociationOrError.getError())
}
dto.existingItem.props.sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
}
if (
dto.itemHash.hasDedicatedKeySystemAssociation() &&
!this.itemIsAlreadyAssociatedWithTheKeySystem(dto.existingItem, dto.itemHash)
) {
const keySystemUuidOrError = Uuid.create(dto.itemHash.props.key_system_identifier as string)
if (keySystemUuidOrError.isFailed()) {
return Result.fail(keySystemUuidOrError.getError())
}
const keySystemUuid = keySystemUuidOrError.getValue()
const keySystemAssociationOrError = KeySystemAssociation.create({
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
timestamps: Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
).getValue(),
keySystemUuid,
})
if (keySystemAssociationOrError.isFailed()) {
return Result.fail(keySystemAssociationOrError.getError())
}
dto.existingItem.props.keySystemAssociation = keySystemAssociationOrError.getValue()
}
if (dto.itemHash.props.deleted === true) {
dto.existingItem.props.deleted = true
dto.existingItem.props.content = null
@ -132,4 +192,18 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
return Result.ok(dto.existingItem)
}
private itemIsAlreadyAssociatedWithTheSharedVault(item: Item, itemHash: ItemHash): boolean {
return (
item.props.sharedVaultAssociation !== undefined &&
item.props.sharedVaultAssociation.props.sharedVaultUuid.value === itemHash.props.shared_vault_uuid
)
}
private itemIsAlreadyAssociatedWithTheKeySystem(item: Item, itemHash: ItemHash): boolean {
return (
item.props.keySystemAssociation !== undefined &&
item.props.keySystemAssociation.props.keySystemUuid.value === itemHash.props.key_system_identifier
)
}
}

View file

@ -5,4 +5,5 @@ export interface UpdateExistingItemDTO {
existingItem: Item
itemHash: ItemHash
sessionUuid: string | null
performingUserUuid: string
}

View file

@ -1,6 +1,6 @@
import { ReadStream } from 'fs'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { MapperInterface } from '@standardnotes/domain-core'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { Item } from '../../Domain/Item/Item'
import { ItemQuery } from '../../Domain/Item/ItemQuery'
@ -78,11 +78,11 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
.execute()
}
async findByUuid(uuid: string): Promise<Item | null> {
async findByUuid(uuid: Uuid): Promise<Item | null> {
const persistence = await this.ormRepository
.createQueryBuilder('item')
.where('item.uuid = :uuid', {
uuid,
uuid: uuid.value,
})
.getOne()
@ -90,7 +90,19 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
return null
}
return this.mapper.toDomain(persistence)
const item = this.mapper.toDomain(persistence)
const keySystemAssociation = await this.keySystemAssociationRepository.findByItemUuid(uuid)
if (keySystemAssociation) {
item.props.keySystemAssociation = keySystemAssociation
}
const sharedVaultAssociation = await this.sharedVaultAssociationRepository.findByItemUuid(uuid)
if (sharedVaultAssociation) {
item.props.sharedVaultAssociation = sharedVaultAssociation
}
return item
}
async findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>> {

View file

@ -1,5 +1,5 @@
import { Repository } from 'typeorm'
import { MapperInterface } from '@standardnotes/domain-core'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation'
import { KeySystemAssociationRepositoryInterface } from '../../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
@ -11,6 +11,21 @@ export class TypeORMKeySystemAssociationRepository implements KeySystemAssociati
private mapper: MapperInterface<KeySystemAssociation, TypeORMKeySystemAssociation>,
) {}
async findByItemUuid(itemUuid: Uuid): Promise<KeySystemAssociation | null> {
const persistence = await this.ormRepository
.createQueryBuilder('key_system_association')
.where('key_system_association.item_uuid = :itemUuid', {
itemUuid: itemUuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async save(keySystemAssociation: KeySystemAssociation): Promise<void> {
await this.ormRepository.save(this.mapper.toProjection(keySystemAssociation))
}

View file

@ -9,7 +9,7 @@ export class TypeORMSharedVault {
name: 'user_uuid',
length: 36,
})
@Index('index_shared_vaults_on_user_uuid')
@Index('user_uuid_on_shared_vaults')
declare userUuid: string
@Column({

View file

@ -1,5 +1,5 @@
import { Repository } from 'typeorm'
import { MapperInterface } from '@standardnotes/domain-core'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation'
import { SharedVaultAssociationRepositoryInterface } from '../../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
@ -11,6 +11,21 @@ export class TypeORMSharedVaultAssociationRepository implements SharedVaultAssoc
private mapper: MapperInterface<SharedVaultAssociation, TypeORMSharedVaultAssociation>,
) {}
async findByItemUuid(itemUuid: Uuid): Promise<SharedVaultAssociation | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_association')
.where('shared_vault_association.item_uuid = :itemUuid', {
itemUuid: itemUuid.value,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async save(sharedVaultAssociation: SharedVaultAssociation): Promise<void> {
await this.ormRepository.save(this.mapper.toProjection(sharedVaultAssociation))
}

View file

@ -1,4 +1,4 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'shared_vault_invites' })
export class TypeORMSharedVaultInvite {
@ -9,18 +9,21 @@ export class TypeORMSharedVaultInvite {
name: 'shared_vault_uuid',
length: 36,
})
@Index('shared_vault_uuid_on_shared_vault_invites')
declare sharedVaultUuid: string
@Column({
name: 'user_uuid',
length: 36,
})
@Index('user_uuid_on_shared_vault_invites')
declare userUuid: string
@Column({
name: 'sender_uuid',
length: 36,
})
@Index('sender_uuid_on_shared_vault_invites')
declare senderUuid: string
@Column({

View file

@ -1,4 +1,4 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'shared_vault_users' })
export class TypeORMSharedVaultUser {
@ -9,12 +9,14 @@ export class TypeORMSharedVaultUser {
name: 'shared_vault_uuid',
length: 36,
})
@Index('shared_vault_uuid_on_shared_vault_users')
declare sharedVaultUuid: string
@Column({
name: 'user_uuid',
length: 36,
})
@Index('user_uuid_on_shared_vault_users')
declare userUuid: string
@Column({