mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
feat(syncing-server): add designated survivors in fetching shared vaults response (#844)
This commit is contained in:
@@ -871,6 +871,7 @@ export class ContainerConfigLoader {
|
||||
.bind<DesignateSurvivor>(TYPES.Sync_DesignateSurvivor)
|
||||
.toConstantValue(
|
||||
new DesignateSurvivor(
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
|
||||
+1
@@ -8,4 +8,5 @@ export interface SharedVaultUserRepositoryInterface {
|
||||
remove(sharedVault: SharedVaultUser): Promise<void>
|
||||
removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
|
||||
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
|
||||
findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null>
|
||||
}
|
||||
|
||||
+35
-1
@@ -5,8 +5,12 @@ import { DesignateSurvivor } from './DesignateSurvivor'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
|
||||
describe('DesignateSurvivor', () => {
|
||||
let sharedVault: SharedVault
|
||||
let sharedVaultRepository: SharedVaultRepositoryInterface
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
let sharedVaultUser: SharedVaultUser
|
||||
let sharedVaultOwner: SharedVaultUser
|
||||
@@ -15,9 +19,25 @@ describe('DesignateSurvivor', () => {
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new DesignateSurvivor(sharedVaultUserRepository, timer, domainEventFactory, domainEventPublisher)
|
||||
new DesignateSurvivor(
|
||||
sharedVaultRepository,
|
||||
sharedVaultUserRepository,
|
||||
timer,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
sharedVault = SharedVault.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
fileUploadBytesUsed: 123,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockReturnValue(sharedVault)
|
||||
sharedVaultRepository.save = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
|
||||
@@ -86,6 +106,20 @@ describe('DesignateSurvivor', () => {
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if shared vault is not found', async () => {
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if shared vault user is not found', async () => {
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner])
|
||||
|
||||
|
||||
+14
@@ -12,9 +12,11 @@ import {
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { DesignateSurvivorDTO } from './DesignateSurvivorDTO'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
|
||||
export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private sharedVaultRepository: SharedVaultRepositoryInterface,
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
@@ -40,6 +42,11 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
}
|
||||
const originatorUuid = originatorUuidOrError.getValue()
|
||||
|
||||
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
|
||||
if (!sharedVault) {
|
||||
return Result.fail('Shared vault not found')
|
||||
}
|
||||
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
let sharedVaultExistingSurvivor: SharedVaultUser | undefined
|
||||
let toBeDesignatedAsASurvivor: SharedVaultUser | undefined
|
||||
@@ -92,6 +99,13 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
}),
|
||||
)
|
||||
|
||||
sharedVault.props.timestamps = Timestamps.create(
|
||||
sharedVault.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
|
||||
await this.sharedVaultRepository.save(sharedVault)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -40,7 +40,7 @@ describe('GetSharedVaults', () => {
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.getValue()).toEqual([sharedVault])
|
||||
expect(result.getValue().sharedVaults).toEqual([sharedVault])
|
||||
})
|
||||
|
||||
it('returns empty array if no shared vaults found', async () => {
|
||||
@@ -52,7 +52,7 @@ describe('GetSharedVaults', () => {
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.getValue()).toEqual([])
|
||||
expect(result.getValue().sharedVaults).toEqual([])
|
||||
})
|
||||
|
||||
it('returns error if user uuid is invalid', async () => {
|
||||
@@ -64,4 +64,17 @@ describe('GetSharedVaults', () => {
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fetch designated survivors if includeDesignatedSurvivors is true', async () => {
|
||||
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
includeDesignatedSurvivors: true,
|
||||
})
|
||||
|
||||
expect(result.getValue().designatedSurvivors).toEqual([sharedVaultUser])
|
||||
})
|
||||
})
|
||||
|
||||
+34
-5
@@ -1,17 +1,28 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Result, SharedVaultUser, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
import { GetSharedVaultsDTO } from './GetSharedVaultsDTO'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
|
||||
export class GetSharedVaults implements UseCaseInterface<SharedVault[]> {
|
||||
export class GetSharedVaults
|
||||
implements
|
||||
UseCaseInterface<{
|
||||
sharedVaults: SharedVault[]
|
||||
designatedSurvivors: SharedVaultUser[]
|
||||
}>
|
||||
{
|
||||
constructor(
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private sharedVaultRepository: SharedVaultRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetSharedVaultsDTO): Promise<Result<SharedVault[]>> {
|
||||
async execute(dto: GetSharedVaultsDTO): Promise<
|
||||
Result<{
|
||||
sharedVaults: SharedVault[]
|
||||
designatedSurvivors: SharedVaultUser[]
|
||||
}>
|
||||
> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
@@ -25,11 +36,29 @@ export class GetSharedVaults implements UseCaseInterface<SharedVault[]> {
|
||||
)
|
||||
|
||||
if (sharedVaultUuids.length === 0) {
|
||||
return Result.ok([])
|
||||
return Result.ok({
|
||||
sharedVaults: [],
|
||||
designatedSurvivors: [],
|
||||
})
|
||||
}
|
||||
|
||||
const sharedVaults = await this.sharedVaultRepository.findByUuids(sharedVaultUuids, dto.lastSyncTime)
|
||||
|
||||
return Result.ok(sharedVaults)
|
||||
const designatedSurvivors = []
|
||||
if (dto.includeDesignatedSurvivors) {
|
||||
for (const sharedVault of sharedVaults) {
|
||||
const designatedSurvivor = await this.sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid(
|
||||
sharedVault.uuid,
|
||||
)
|
||||
if (designatedSurvivor) {
|
||||
designatedSurvivors.push(designatedSurvivor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
sharedVaults,
|
||||
designatedSurvivors,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
export interface GetSharedVaultsDTO {
|
||||
userUuid: string
|
||||
includeDesignatedSurvivors?: boolean
|
||||
lastSyncTime?: number
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('SyncItems', () => {
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
getSharedVaultsUseCase = {} as jest.Mocked<GetSharedVaults>
|
||||
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
|
||||
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sharedVaults: [], designatedSurivors: [] }))
|
||||
|
||||
getSharedVaultInvitesSentToUserUseCase = {} as jest.Mocked<GetSharedVaultInvitesSentToUser>
|
||||
getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
|
||||
|
||||
@@ -73,12 +73,13 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
|
||||
const sharedVaultsOrError = await this.getSharedVaultsUseCase.execute({
|
||||
userUuid: dto.userUuid,
|
||||
includeDesignatedSurvivors: false,
|
||||
lastSyncTime: getItemsResult.lastSyncTime ?? undefined,
|
||||
})
|
||||
if (sharedVaultsOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultsOrError.getError())
|
||||
}
|
||||
const sharedVaults = sharedVaultsOrError.getValue()
|
||||
const sharedVaultsResult = sharedVaultsOrError.getValue()
|
||||
|
||||
const sharedVaultInvitesOrError = await this.getSharedVaultInvitesSentToUserUseCase.execute({
|
||||
userUuid: dto.userUuid,
|
||||
@@ -114,7 +115,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
conflicts: saveItemsResult.conflicts,
|
||||
cursorToken: getItemsResult.cursorToken,
|
||||
sharedVaultInvites,
|
||||
sharedVaults,
|
||||
sharedVaults: sharedVaultsResult.sharedVaults,
|
||||
messages,
|
||||
notifications,
|
||||
}
|
||||
|
||||
+11
-4
@@ -36,23 +36,30 @@ export class BaseSharedVaultsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSharedVaults(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.getSharedVaultsUseCase.execute({
|
||||
const resultOrError = await this.getSharedVaultsUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
includeDesignatedSurvivors: true,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: result.getError(),
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
HttpStatusCode.BadRequest,
|
||||
)
|
||||
}
|
||||
|
||||
const result = resultOrError.getValue()
|
||||
|
||||
return this.json({
|
||||
sharedVaults: result.getValue().map((sharedVault) => this.sharedVaultHttpMapper.toProjection(sharedVault)),
|
||||
sharedVaults: result.sharedVaults.map((sharedVault) => this.sharedVaultHttpMapper.toProjection(sharedVault)),
|
||||
designatedSurvivors: result.designatedSurvivors.map((designatedSurvivor) => ({
|
||||
sharedVaultUuid: designatedSurvivor.props.sharedVaultUuid.value,
|
||||
userUuid: designatedSurvivor.props.userUuid.value,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,24 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito
|
||||
private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
|
||||
) {}
|
||||
|
||||
async findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('shared_vault_user')
|
||||
.where('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', {
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
})
|
||||
.andWhere('shared_vault_user.is_designated_survivor = :isDesignatedSurvivor', {
|
||||
isDesignatedSurvivor: true,
|
||||
})
|
||||
.getOne()
|
||||
|
||||
if (persistence === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.mapper.toDomain(persistence)
|
||||
}
|
||||
|
||||
async removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder('shared_vault_user')
|
||||
|
||||
Reference in New Issue
Block a user