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:
parent
b32f851a90
commit
3b804e2321
19 changed files with 488 additions and 20 deletions
|
@ -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`')
|
||||
}
|
||||
}
|
|
@ -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"')
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,4 +5,5 @@ export interface UpdateExistingItemDTO {
|
|||
existingItem: Item
|
||||
itemHash: ItemHash
|
||||
sessionUuid: string | null
|
||||
performingUserUuid: string
|
||||
}
|
||||
|
|
|
@ -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 }>> {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in a new issue