mirror of
https://github.com/standardnotes/server
synced 2026-01-24 17:01:09 -05:00
Compare commits
21 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7cf53722c | ||
|
|
8cb33dc906 | ||
|
|
1d73e4f072 | ||
|
|
0a0f82ea3d | ||
|
|
f9b1f40ddf | ||
|
|
0562b0a621 | ||
|
|
15ed1fd789 | ||
|
|
5001496c7b | ||
|
|
0a1080ce2a | ||
|
|
4802d7e876 | ||
|
|
bcd95cdbe9 | ||
|
|
d50c4440c2 | ||
|
|
921c30f641 | ||
|
|
22540ee834 | ||
|
|
4f4443a882 | ||
|
|
80dbacf933 | ||
|
|
dc77ff3e45 | ||
|
|
6515dcf487 | ||
|
|
d0fd6b98df | ||
|
|
345efacb44 | ||
|
|
d0dba1b66d |
4
.github/workflows/common-e2e.yml
vendored
4
.github/workflows/common-e2e.yml
vendored
@@ -24,7 +24,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
secondary_db_enabled: [true, false]
|
||||
transition_mode_enabled: [true, false]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
@@ -51,7 +50,6 @@ jobs:
|
||||
DB_TYPE: mysql
|
||||
CACHE_TYPE: redis
|
||||
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
|
||||
TRANSITION_MODE_ENABLED: ${{ matrix.transition_mode_enabled }}
|
||||
|
||||
- name: Wait for server to start
|
||||
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
|
||||
@@ -75,7 +73,6 @@ jobs:
|
||||
db_type: [mysql, sqlite]
|
||||
cache_type: [redis, memory]
|
||||
secondary_db_enabled: [true, false]
|
||||
transition_mode_enabled: [true, false]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -145,7 +142,6 @@ jobs:
|
||||
echo "REDIS_URL=redis://localhost:6379" >> packages/home-server/.env
|
||||
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
|
||||
echo "SECONDARY_DB_ENABLED=${{ matrix.secondary_db_enabled }}" >> packages/home-server/.env
|
||||
echo "TRANSITION_MODE_ENABLED=${{ matrix.transition_mode_enabled }}" >> packages/home-server/.env
|
||||
echo "MONGO_HOST=localhost" >> packages/home-server/.env
|
||||
echo "MONGO_PORT=27017" >> packages/home-server/.env
|
||||
echo "MONGO_DATABASE=standardnotes" >> packages/home-server/.env
|
||||
|
||||
@@ -24,7 +24,6 @@ services:
|
||||
DB_TYPE: "${DB_TYPE}"
|
||||
CACHE_TYPE: "${CACHE_TYPE}"
|
||||
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
|
||||
TRANSITION_MODE_ENABLED: "${TRANSITION_MODE_ENABLED}"
|
||||
container_name: server-ci
|
||||
ports:
|
||||
- 3123:3000
|
||||
|
||||
@@ -68,9 +68,6 @@ fi
|
||||
if [ -z "$SECONDARY_DB_ENABLED" ]; then
|
||||
export SECONDARY_DB_ENABLED=false
|
||||
fi
|
||||
if [ -z "$TRANSITION_MODE_ENABLED" ]; then
|
||||
export TRANSITION_MODE_ENABLED=false
|
||||
fi
|
||||
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
|
||||
|
||||
#########
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts$',
|
||||
testTimeout: 20000,
|
||||
coverageReporters: ['text-summary'],
|
||||
coverageReporters: ['text'],
|
||||
reporters: ['summary'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.26.21](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.20...@standardnotes/analytics@2.26.21) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.26.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.19...@standardnotes/analytics@2.26.20) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.26.20",
|
||||
"version": "2.26.21",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.75.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.75.0...@standardnotes/api-gateway@1.75.1) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.75.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.17...@standardnotes/api-gateway@1.75.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.75.0",
|
||||
"version": "1.75.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.146.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.146.0...@standardnotes/auth-server@1.146.1) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.146.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.145.0...@standardnotes/auth-server@1.146.0) (2023-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** register specs ([f9b1f40](https://github.com/standardnotes/server/commit/f9b1f40ddf2d733d106ea64b9a7c4b38c5ec43ce))
|
||||
|
||||
### Features
|
||||
|
||||
* remove transition mode from code ([5001496](https://github.com/standardnotes/server/commit/5001496c7bc1df9e20c2d88ebf52ed77f868110c))
|
||||
|
||||
# [1.145.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.144.0...@standardnotes/auth-server@1.145.0) (2023-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* remove user from all shared vaults upon account deletion ([#843](https://github.com/standardnotes/server/issues/843)) ([dc77ff3](https://github.com/standardnotes/server/commit/dc77ff3e45983d231bc9c132802428e77b4be431))
|
||||
|
||||
# [1.144.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.143.9...@standardnotes/auth-server@1.144.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.144.0",
|
||||
"version": "1.146.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -595,9 +595,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind(TYPES.Auth_READONLY_USERS)
|
||||
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
|
||||
container
|
||||
.bind(TYPES.Auth_TRANSITION_MODE_ENABLED)
|
||||
.toConstantValue(env.get('TRANSITION_MODE_ENABLED', true) === 'true')
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
@@ -1008,7 +1005,16 @@ export class ContainerConfigLoader {
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.Auth_UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.Auth_AccountDeletionRequestedEventHandler)
|
||||
.to(AccountDeletionRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<SessionRepositoryInterface>(TYPES.Auth_SessionRepository),
|
||||
container.get<EphemeralSessionRepositoryInterface>(TYPES.Auth_EphemeralSessionRepository),
|
||||
container.get<RevokedSessionRepositoryInterface>(TYPES.Auth_RevokedSessionRepository),
|
||||
container.get<RemoveSharedVaultUser>(TYPES.Auth_RemoveSharedVaultUser),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionPurchasedEventHandler>(TYPES.Auth_SubscriptionPurchasedEventHandler)
|
||||
.to(SubscriptionPurchasedEventHandler)
|
||||
|
||||
@@ -105,7 +105,6 @@ const TYPES = {
|
||||
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
|
||||
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
|
||||
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
|
||||
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
|
||||
// use cases
|
||||
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
|
||||
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { EphemeralSession } from '../Session/EphemeralSession'
|
||||
import { EphemeralSessionRepositoryInterface } from '../Session/EphemeralSessionRepositoryInterface'
|
||||
import { RevokedSession } from '../Session/RevokedSession'
|
||||
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
|
||||
import { Session } from '../Session/Session'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
|
||||
|
||||
describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
let ephemeralSessionRepository: EphemeralSessionRepositoryInterface
|
||||
let revokedSessionRepository: RevokedSessionRepositoryInterface
|
||||
let logger: Logger
|
||||
let session: Session
|
||||
let ephemeralSession: EphemeralSession
|
||||
let revokedSession: RevokedSession
|
||||
let user: User
|
||||
let event: AccountDeletionRequestedEvent
|
||||
|
||||
const createHandler = () =>
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
userRepository,
|
||||
sessionRepository,
|
||||
ephemeralSessionRepository,
|
||||
revokedSessionRepository,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
userRepository.remove = jest.fn()
|
||||
|
||||
session = {
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<Session>
|
||||
|
||||
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
|
||||
sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session])
|
||||
sessionRepository.remove = jest.fn()
|
||||
|
||||
ephemeralSession = {
|
||||
uuid: '2-3-4',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
} as jest.Mocked<EphemeralSession>
|
||||
|
||||
ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
|
||||
ephemeralSessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([ephemeralSession])
|
||||
ephemeralSessionRepository.deleteOne = jest.fn()
|
||||
|
||||
revokedSession = {
|
||||
uuid: '3-4-5',
|
||||
} as jest.Mocked<RevokedSession>
|
||||
|
||||
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
|
||||
revokedSessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([revokedSession])
|
||||
revokedSessionRepository.remove = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '2-3-4',
|
||||
roleNames: ['CORE_USER'],
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove a user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userRepository.remove).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should not remove a user with invalid uuid', async () => {
|
||||
event.payload.userUuid = 'invalid'
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userRepository.remove).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not remove a user if one does not exist', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userRepository.remove).not.toHaveBeenCalled()
|
||||
expect(sessionRepository.remove).not.toHaveBeenCalled()
|
||||
expect(revokedSessionRepository.remove).not.toHaveBeenCalled()
|
||||
expect(ephemeralSessionRepository.deleteOne).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove all user sessions', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(sessionRepository.remove).toHaveBeenCalledWith(session)
|
||||
expect(revokedSessionRepository.remove).toHaveBeenCalledWith(revokedSession)
|
||||
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '00000000-0000-0000-0000-000000000000')
|
||||
})
|
||||
})
|
||||
@@ -1,22 +1,21 @@
|
||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
|
||||
import { EphemeralSessionRepositoryInterface } from '../Session/EphemeralSessionRepositoryInterface'
|
||||
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { RemoveSharedVaultUser } from '../UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser'
|
||||
|
||||
@injectable()
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_EphemeralSessionRepository)
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private sessionRepository: SessionRepositoryInterface,
|
||||
private ephemeralSessionRepository: EphemeralSessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
private removeSharedVaultUser: RemoveSharedVaultUser,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
@@ -38,6 +37,13 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
|
||||
await this.removeSessions(userUuid.value)
|
||||
|
||||
const result = await this.removeSharedVaultUser.execute({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not remove shared vault user: ${result.getError()}`)
|
||||
}
|
||||
|
||||
await this.userRepository.remove(user)
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${userUuid.value}`)
|
||||
|
||||
@@ -10,6 +10,12 @@ export class UserRemovedFromSharedVaultEventHandler implements DomainEventHandle
|
||||
) {}
|
||||
|
||||
async handle(event: UserRemovedFromSharedVaultEvent): Promise<void> {
|
||||
if (!event.payload.sharedVaultUuid) {
|
||||
this.logger.error(`Shared vault uuid is missing from event: ${JSON.stringify(event)}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const result = await this.removeSharedVaultUser.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
sharedVaultUuid: event.payload.sharedVaultUuid,
|
||||
|
||||
@@ -21,19 +21,9 @@ describe('Register', () => {
|
||||
let user: User
|
||||
let crypter: CrypterInterface
|
||||
let timer: TimerInterface
|
||||
let transitionModeEnabled = false
|
||||
|
||||
const createUseCase = () =>
|
||||
new Register(
|
||||
userRepository,
|
||||
roleRepository,
|
||||
authResponseFactory,
|
||||
crypter,
|
||||
false,
|
||||
settingService,
|
||||
timer,
|
||||
transitionModeEnabled,
|
||||
)
|
||||
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
@@ -94,45 +84,7 @@ describe('Register', () => {
|
||||
expect(settingService.applyDefaultSettingsUponRegistration).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should register a new user with default role', async () => {
|
||||
const role = new Role()
|
||||
role.name = 'role1'
|
||||
roleRepository.findOneByName = jest.fn().mockReturnValue(role)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
apiVersion: '20200115',
|
||||
ephemeralSession: false,
|
||||
version: '004',
|
||||
pwCost: 11,
|
||||
pwSalt: 'qweqwe',
|
||||
pwNonce: undefined,
|
||||
}),
|
||||
).toEqual({ success: true, authResponse: { foo: 'bar' } })
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
email: 'test@test.te',
|
||||
encryptedPassword: expect.any(String),
|
||||
encryptedServerKey: 'test',
|
||||
serverEncryptionVersion: 1,
|
||||
pwCost: 11,
|
||||
pwNonce: undefined,
|
||||
pwSalt: 'qweqwe',
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
uuid: expect.any(String),
|
||||
version: '004',
|
||||
createdAt: new Date(1),
|
||||
updatedAt: new Date(1),
|
||||
roles: Promise.resolve([role]),
|
||||
})
|
||||
})
|
||||
|
||||
it('should register a new user with default role and transition role', async () => {
|
||||
transitionModeEnabled = true
|
||||
|
||||
const role = new Role()
|
||||
role.name = RoleName.NAMES.CoreUser
|
||||
|
||||
@@ -249,7 +201,6 @@ describe('Register', () => {
|
||||
true,
|
||||
settingService,
|
||||
timer,
|
||||
transitionModeEnabled,
|
||||
).execute({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
|
||||
@@ -27,7 +27,6 @@ export class Register implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_TRANSITION_MODE_ENABLED) private transitionModeEnabled: boolean,
|
||||
) {}
|
||||
|
||||
async execute(dto: RegisterDTO): Promise<RegisterResponse> {
|
||||
@@ -78,11 +77,9 @@ export class Register implements UseCaseInterface {
|
||||
if (defaultRole) {
|
||||
roles.push(defaultRole)
|
||||
}
|
||||
if (this.transitionModeEnabled) {
|
||||
const transitionRole = await this.roleRepository.findOneByName(RoleName.NAMES.TransitionUser)
|
||||
if (transitionRole) {
|
||||
roles.push(transitionRole)
|
||||
}
|
||||
const transitionRole = await this.roleRepository.findOneByName(RoleName.NAMES.TransitionUser)
|
||||
if (transitionRole) {
|
||||
roles.push(transitionRole)
|
||||
}
|
||||
user.roles = Promise.resolve(roles)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('RemoveSharedVaultUser', () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<SharedVaultUser>)
|
||||
sharedVaultUserRepository.findByUserUuid = jest.fn().mockReturnValue([{} as jest.Mocked<SharedVaultUser>])
|
||||
sharedVaultUserRepository.remove = jest.fn()
|
||||
})
|
||||
|
||||
@@ -28,6 +29,17 @@ describe('RemoveSharedVaultUser', () => {
|
||||
expect(sharedVaultUserRepository.remove).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove all shared vault users', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(sharedVaultUserRepository.remove).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail when user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
|
||||
@@ -13,21 +13,31 @@ export class RemoveSharedVaultUser implements UseCaseInterface<void> {
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
|
||||
const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
|
||||
userUuid,
|
||||
sharedVaultUuid,
|
||||
})
|
||||
if (!sharedVaultUser) {
|
||||
return Result.fail('Shared vault user not found')
|
||||
let sharedVaultUuid: Uuid | undefined
|
||||
if (dto.sharedVaultUuid !== undefined) {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
}
|
||||
|
||||
await this.sharedVaultUserRepository.remove(sharedVaultUser)
|
||||
if (sharedVaultUuid) {
|
||||
const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
|
||||
userUuid,
|
||||
sharedVaultUuid,
|
||||
})
|
||||
if (!sharedVaultUser) {
|
||||
return Result.fail('Shared vault user not found')
|
||||
}
|
||||
|
||||
await this.sharedVaultUserRepository.remove(sharedVaultUser)
|
||||
} else {
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findByUserUuid(userUuid)
|
||||
for (const sharedVaultUser of sharedVaultUsers) {
|
||||
await this.sharedVaultUserRepository.remove(sharedVaultUser)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface RemoveSharedVaultUserDTO {
|
||||
sharedVaultUuid: string
|
||||
sharedVaultUuid?: string
|
||||
userUuid: string
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.33](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.32...@standardnotes/domain-events-infra@1.12.33) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.32](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.31...@standardnotes/domain-events-infra@1.12.32) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.32",
|
||||
"version": "1.12.33",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.129.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.129.0...@standardnotes/domain-events@2.129.1) (2023-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remember paging progress on transitioning ([1d73e4f](https://github.com/standardnotes/server/commit/1d73e4f0720d41029af4d4b2b7a10d101add6c82))
|
||||
|
||||
# [2.129.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.128.0...@standardnotes/domain-events@2.129.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.129.0",
|
||||
"version": "2.129.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,4 +3,5 @@ export interface TransitionStatusUpdatedEventPayload {
|
||||
transitionType: 'items' | 'revisions'
|
||||
transitionTimestamp: number
|
||||
status: string
|
||||
page?: number
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.49](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.48...@standardnotes/event-store@1.11.49) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.48](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.47...@standardnotes/event-store@1.11.48) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.48",
|
||||
"version": "1.11.49",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.28](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.27...@standardnotes/files-server@1.22.28) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.22.27](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.26...@standardnotes/files-server@1.22.27) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.22.27",
|
||||
"version": "1.22.28",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -16,5 +16,3 @@ MONGO_PORT=27017
|
||||
MONGO_USERNAME=standardnotes
|
||||
MONGO_PASSWORD=standardnotes
|
||||
MONGO_DATABASE=standardnotes
|
||||
|
||||
TRANSITION_MODE_ENABLED=false
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.16.1](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.0...@standardnotes/home-server@1.16.1) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.80...@standardnotes/home-server@1.16.0) (2023-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* remove transition mode from code ([5001496](https://github.com/standardnotes/server/commit/5001496c7bc1df9e20c2d88ebf52ed77f868110c))
|
||||
|
||||
## [1.15.80](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.79...@standardnotes/home-server@1.15.80) (2023-09-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.79](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.78...@standardnotes/home-server@1.15.79) (2023-09-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.78](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.77...@standardnotes/home-server@1.15.78) (2023-09-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.77](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.76...@standardnotes/home-server@1.15.77) (2023-09-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.76](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.75...@standardnotes/home-server@1.15.76) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.75](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.74...@standardnotes/home-server@1.15.75) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.15.75",
|
||||
"version": "1.16.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.36.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.6...@standardnotes/revisions-server@1.36.7) (2023-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remember paging progress on transitioning ([1d73e4f](https://github.com/standardnotes/server/commit/1d73e4f0720d41029af4d4b2b7a10d101add6c82))
|
||||
|
||||
## [1.36.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.5...@standardnotes/revisions-server@1.36.6) (2023-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add more logs to transition process ([0562b0a](https://github.com/standardnotes/server/commit/0562b0a621eb878026fbdc0346b6170e815b64bf))
|
||||
* remove excessive logs ([15ed1fd](https://github.com/standardnotes/server/commit/15ed1fd789aba306cbec6a23e88d5c1f837dabc0))
|
||||
|
||||
## [1.36.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.4...@standardnotes/revisions-server@1.36.5) (2023-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable cleaning secondary database on transition ([4f4443a](https://github.com/standardnotes/server/commit/4f4443a882f69c2e76ef831ef36347c9c54f31cd))
|
||||
* integrity check during transition ([921c30f](https://github.com/standardnotes/server/commit/921c30f6415ef122a7d1af83ffa3f6840a42edba))
|
||||
* processing migration optimization ([22540ee](https://github.com/standardnotes/server/commit/22540ee83436b986949127a6923285a702162706))
|
||||
|
||||
## [1.36.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.3...@standardnotes/revisions-server@1.36.4) (2023-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** add log info about skipping already existing revision ([d0dba1b](https://github.com/standardnotes/server/commit/d0dba1b66df0fb4ab64ede8f0d4e1c4e2a23ad3c))
|
||||
|
||||
## [1.36.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.2...@standardnotes/revisions-server@1.36.3) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.36.3",
|
||||
"version": "1.36.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -5,9 +5,10 @@ import { Logger } from 'winston'
|
||||
|
||||
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO } from './TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO'
|
||||
import { RevisionRepositoryInterface } from '../../../Revision/RevisionRepositoryInterface'
|
||||
import { Revision } from '../../../Revision/Revision'
|
||||
|
||||
export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
|
||||
private readonly pagingProgress: Map<string, number> = new Map()
|
||||
|
||||
constructor(
|
||||
private primaryRevisionsRepository: RevisionRepositoryInterface,
|
||||
private secondRevisionsRepository: RevisionRepositoryInterface | null,
|
||||
@@ -29,76 +30,27 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
let newRevisionsInSecondaryCount = 0
|
||||
let updatedRevisionsInSecondary: string[] = []
|
||||
let alreadyIdenticalInSecondaryAndPrimary: string[] = []
|
||||
if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
|
||||
const { alreadyExistingInSecondaryAndPrimary, newRevisionsInSecondary, updatedInSecondary } =
|
||||
await this.getNewRevisionsCreatedInSecondaryDatabase(userUuid)
|
||||
|
||||
this.logger.info(
|
||||
`[${dto.userUuid}] ${alreadyExistingInSecondaryAndPrimary.length} already existing identical revisions in primary and secondary.`,
|
||||
)
|
||||
|
||||
alreadyIdenticalInSecondaryAndPrimary = alreadyExistingInSecondaryAndPrimary
|
||||
|
||||
if (newRevisionsInSecondary.length > 0) {
|
||||
this.logger.info(
|
||||
`[${dto.userUuid}] Found ${newRevisionsInSecondary.length} new revisions in secondary database`,
|
||||
)
|
||||
}
|
||||
|
||||
newRevisionsInSecondaryCount = newRevisionsInSecondary.length
|
||||
|
||||
if (updatedInSecondary.length > 0) {
|
||||
this.logger.info(`[${dto.userUuid}] Found ${updatedInSecondary.length} updated revisions in secondary database`)
|
||||
}
|
||||
|
||||
updatedRevisionsInSecondary = updatedInSecondary
|
||||
}
|
||||
|
||||
const updatedRevisionsInSecondaryCount = updatedRevisionsInSecondary.length
|
||||
|
||||
const migrationTimeStart = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Migrating revisions`)
|
||||
|
||||
const migrationResult = await this.migrateRevisionsForUser(
|
||||
userUuid,
|
||||
updatedRevisionsInSecondary,
|
||||
alreadyIdenticalInSecondaryAndPrimary,
|
||||
)
|
||||
const migrationResult = await this.migrateRevisionsForUser(userUuid)
|
||||
if (migrationResult.isFailed()) {
|
||||
if (newRevisionsInSecondaryCount === 0 && updatedRevisionsInSecondaryCount === 0) {
|
||||
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
|
||||
if (cleanupResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Failed to clean up secondary database revisions: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.fail(migrationResult.getError())
|
||||
}
|
||||
const revisionsToSkipInIntegrityCheck = migrationResult.getValue()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Revisions migrated`)
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Checking integrity between primary and secondary database`)
|
||||
|
||||
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
|
||||
userUuid,
|
||||
newRevisionsInSecondaryCount,
|
||||
updatedRevisionsInSecondary,
|
||||
alreadyIdenticalInSecondaryAndPrimary,
|
||||
revisionsToSkipInIntegrityCheck,
|
||||
)
|
||||
if (integrityCheckResult.isFailed()) {
|
||||
if (newRevisionsInSecondaryCount === 0 && updatedRevisionsInSecondaryCount === 0) {
|
||||
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
|
||||
if (cleanupResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Failed to clean up secondary database revisions: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.fail(integrityCheckResult.getError())
|
||||
}
|
||||
|
||||
@@ -119,15 +71,21 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async migrateRevisionsForUser(
|
||||
userUuid: Uuid,
|
||||
updatedRevisionsInSecondary: string[],
|
||||
alreadyExistingInSecondaryAndPrimary: string[],
|
||||
): Promise<Result<void>> {
|
||||
private async migrateRevisionsForUser(userUuid: Uuid): Promise<Result<string[]>> {
|
||||
try {
|
||||
if (!this.pagingProgress.has(userUuid.value)) {
|
||||
this.pagingProgress.set(userUuid.value, 1)
|
||||
}
|
||||
const initialPage = this.pagingProgress.get(userUuid.value) as number
|
||||
|
||||
this.logger.info(`[${userUuid.value}] Migrating from page ${initialPage}`)
|
||||
|
||||
const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
const totalPages = Math.ceil(totalRevisionsCountForUser / this.pageSize)
|
||||
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
|
||||
const revisionsToSkipInIntegrityCheck = []
|
||||
for (let currentPage = initialPage; currentPage <= totalPages; currentPage++) {
|
||||
this.pagingProgress.set(userUuid.value, currentPage)
|
||||
|
||||
const query = {
|
||||
userUuid: userUuid,
|
||||
offset: (currentPage - 1) * this.pageSize,
|
||||
@@ -135,35 +93,44 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
|
||||
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
|
||||
|
||||
for (const revision of revisions) {
|
||||
try {
|
||||
if (
|
||||
updatedRevisionsInSecondary.find((updatedRevisionUuid) => updatedRevisionUuid === revision.id.toString())
|
||||
) {
|
||||
const revisionInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).findOneByUuid(Uuid.create(revision.id.toString()).getValue(), revision.props.userUuid as Uuid, [])
|
||||
|
||||
if (revisionInSecondary !== null) {
|
||||
if (revisionInSecondary.isIdenticalTo(revision)) {
|
||||
continue
|
||||
}
|
||||
if (revisionInSecondary.props.dates.updatedAt > revision.props.dates.updatedAt) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Revision ${revision.id.toString()} is older than revision in secondary database`,
|
||||
)
|
||||
revisionsToSkipInIntegrityCheck.push(revision.id.toString())
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[${
|
||||
userUuid.value
|
||||
}] Skipping saving revision ${revision.id.toString()} as it was updated in secondary database`,
|
||||
}] Removing revision ${revision.id.toString()} in secondary database as it is not identical to revision in primary database`,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
alreadyExistingInSecondaryAndPrimary.find(
|
||||
(alreadyExistingRevisionUuid) => alreadyExistingRevisionUuid === revision.id.toString(),
|
||||
await (this.secondRevisionsRepository as RevisionRepositoryInterface).removeOneByUuid(
|
||||
Uuid.create(revisionInSecondary.id.toString()).getValue(),
|
||||
revisionInSecondary.props.userUuid as Uuid,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
}
|
||||
|
||||
const didSave = await (this.secondRevisionsRepository as RevisionRepositoryInterface).insert(revision)
|
||||
if (!didSave) {
|
||||
return Result.fail(`Failed to save revision ${revision.id.toString()} to secondary database`)
|
||||
this.logger.error(`Failed to save revision ${revision.id.toString()} to secondary database`)
|
||||
}
|
||||
} catch (error) {
|
||||
return Result.fail(
|
||||
this.logger.error(
|
||||
`Errored when saving revision ${revision.id.toString()} to secondary database: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
@@ -172,7 +139,7 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
return Result.ok(revisionsToSkipInIntegrityCheck)
|
||||
} catch (error) {
|
||||
return Result.fail(`Errored when migrating revisions for user ${userUuid.value}: ${(error as Error).message}`)
|
||||
}
|
||||
@@ -183,6 +150,8 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
revisionRepository: RevisionRepositoryInterface,
|
||||
): Promise<Result<void>> {
|
||||
try {
|
||||
this.logger.info(`[${userUuid.value}] Deleting all revisions from primary database`)
|
||||
|
||||
await revisionRepository.removeByUserUuid(userUuid)
|
||||
|
||||
return Result.ok()
|
||||
@@ -196,115 +165,21 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
await this.timer.sleep(twoSecondsInMilliseconds)
|
||||
}
|
||||
|
||||
private async hasAlreadyDataInSecondaryDatabase(userUuid: Uuid): Promise<boolean> {
|
||||
const totalRevisionsCountForUserInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).countByUserUuid(userUuid)
|
||||
|
||||
const hasAlreadyDataInSecondaryDatabase = totalRevisionsCountForUserInSecondary > 0
|
||||
if (hasAlreadyDataInSecondaryDatabase) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] User has already ${totalRevisionsCountForUserInSecondary} revisions in secondary database`,
|
||||
)
|
||||
}
|
||||
|
||||
return hasAlreadyDataInSecondaryDatabase
|
||||
}
|
||||
|
||||
private async getNewRevisionsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
|
||||
alreadyExistingInSecondaryAndPrimary: string[]
|
||||
newRevisionsInSecondary: string[]
|
||||
updatedInSecondary: string[]
|
||||
}> {
|
||||
this.logger.info(`[${userUuid.value}] Checking for new revisions created in secondary database`)
|
||||
|
||||
const totalRevisionsCountForUser = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).countByUserUuid(userUuid)
|
||||
const totalPages = Math.ceil(totalRevisionsCountForUser / this.pageSize)
|
||||
|
||||
const alreadyExistingInSecondaryAndPrimary: string[] = []
|
||||
const newRevisionsInSecondary: string[] = []
|
||||
const updatedInSecondary: string[] = []
|
||||
|
||||
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
|
||||
const query = {
|
||||
userUuid: userUuid,
|
||||
offset: (currentPage - 1) * this.pageSize,
|
||||
limit: this.pageSize,
|
||||
}
|
||||
|
||||
const revisions = await (this.secondRevisionsRepository as RevisionRepositoryInterface).findByUserUuid(query)
|
||||
for (const revision of revisions) {
|
||||
const { identicalRevisionInPrimary, newerRevisionInSecondary } =
|
||||
await this.checkIfRevisionExistsInPrimaryDatabase(revision)
|
||||
if (identicalRevisionInPrimary !== null) {
|
||||
alreadyExistingInSecondaryAndPrimary.push(revision.id.toString())
|
||||
continue
|
||||
}
|
||||
if (newerRevisionInSecondary !== null) {
|
||||
updatedInSecondary.push(newerRevisionInSecondary.id.toString())
|
||||
continue
|
||||
}
|
||||
if (identicalRevisionInPrimary === null && newerRevisionInSecondary === null) {
|
||||
newRevisionsInSecondary.push(revision.id.toString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
alreadyExistingInSecondaryAndPrimary,
|
||||
newRevisionsInSecondary,
|
||||
updatedInSecondary,
|
||||
}
|
||||
}
|
||||
|
||||
private async checkIfRevisionExistsInPrimaryDatabase(
|
||||
revision: Revision,
|
||||
): Promise<{ identicalRevisionInPrimary: Revision | null; newerRevisionInSecondary: Revision | null }> {
|
||||
const revisionInPrimary = await this.primaryRevisionsRepository.findOneByUuid(
|
||||
Uuid.create(revision.id.toString()).getValue(),
|
||||
revision.props.userUuid as Uuid,
|
||||
[],
|
||||
)
|
||||
|
||||
if (revisionInPrimary === null) {
|
||||
return {
|
||||
identicalRevisionInPrimary: null,
|
||||
newerRevisionInSecondary: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (!revision.isIdenticalTo(revisionInPrimary)) {
|
||||
this.logger.error(
|
||||
`[${revision.props.userUuid
|
||||
?.value}] Revision ${revision.id.toString()} is not identical in primary and secondary database. Revision in secondary database: ${JSON.stringify(
|
||||
revision,
|
||||
)}, revision in primary database: ${JSON.stringify(revisionInPrimary)}`,
|
||||
)
|
||||
|
||||
return {
|
||||
identicalRevisionInPrimary: null,
|
||||
newerRevisionInSecondary:
|
||||
revision.props.dates.updatedAt > revisionInPrimary.props.dates.updatedAt ? revision : null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
identicalRevisionInPrimary: revisionInPrimary,
|
||||
newerRevisionInSecondary: null,
|
||||
}
|
||||
}
|
||||
|
||||
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
|
||||
userUuid: Uuid,
|
||||
newRevisionsInSecondaryCount: number,
|
||||
updatedRevisionsInSecondary: string[],
|
||||
alreadyExistingInSecondaryAndPrimary: string[],
|
||||
revisionsToSkipInIntegrityCheck: string[],
|
||||
): Promise<Result<boolean>> {
|
||||
try {
|
||||
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
const totalRevisionsCountForUserInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).countByUserUuid(userUuid)
|
||||
|
||||
if (totalRevisionsCountForUserInPrimary > totalRevisionsCountForUserInSecondary) {
|
||||
return Result.fail(
|
||||
`Total revisions count for user ${userUuid.value} in primary database (${totalRevisionsCountForUserInPrimary}) does not match total revisions count in secondary database (${totalRevisionsCountForUserInSecondary})`,
|
||||
)
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(totalRevisionsCountForUserInPrimary / this.pageSize)
|
||||
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
|
||||
@@ -331,22 +206,7 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
return Result.fail(`Revision ${revision.id.toString()} not found in secondary database`)
|
||||
}
|
||||
|
||||
if (
|
||||
updatedRevisionsInSecondary.find((updatedRevisionUuid) => updatedRevisionUuid === revision.id.toString())
|
||||
) {
|
||||
this.logger.info(
|
||||
`[${
|
||||
userUuid.value
|
||||
}] Skipping integrity check for revision ${revision.id.toString()} as it was updated in secondary database`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
alreadyExistingInSecondaryAndPrimary.find(
|
||||
(alreadyExistingRevisionUuid) => alreadyExistingRevisionUuid === revision.id.toString(),
|
||||
)
|
||||
) {
|
||||
if (revisionsToSkipInIntegrityCheck.includes(revision.id.toString())) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -360,19 +220,6 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
}
|
||||
|
||||
const totalRevisionsCountForUserInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).countByUserUuid(userUuid)
|
||||
|
||||
if (
|
||||
totalRevisionsCountForUserInPrimary + newRevisionsInSecondaryCount !==
|
||||
totalRevisionsCountForUserInSecondary
|
||||
) {
|
||||
return Result.fail(
|
||||
`Total revisions count for user ${userUuid.value} in primary database (${totalRevisionsCountForUserInPrimary} + ${newRevisionsInSecondaryCount}) does not match total revisions count in secondary database (${totalRevisionsCountForUserInSecondary})`,
|
||||
)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
} catch (error) {
|
||||
return Result.fail(
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.53](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.52...@standardnotes/scheduler-server@1.20.53) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.52](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.51...@standardnotes/scheduler-server@1.20.52) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.52",
|
||||
"version": "1.20.53",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,50 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.105.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.105.0...@standardnotes/syncing-server@1.105.1) (2023-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add paging progress log ([8cb33dc](https://github.com/standardnotes/syncing-server-js/commit/8cb33dc906391ee8b1ebd333937045c328e4fc06))
|
||||
* remember paging progress on transitioning ([1d73e4f](https://github.com/standardnotes/syncing-server-js/commit/1d73e4f0720d41029af4d4b2b7a10d101add6c82))
|
||||
|
||||
# [1.105.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.104.0...@standardnotes/syncing-server@1.105.0) (2023-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add more logs to transition process ([0562b0a](https://github.com/standardnotes/syncing-server-js/commit/0562b0a621eb878026fbdc0346b6170e815b64bf))
|
||||
* remove excessive logs ([15ed1fd](https://github.com/standardnotes/syncing-server-js/commit/15ed1fd789aba306cbec6a23e88d5c1f837dabc0))
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** transfer shared vault ownership to designated survivor upon account deletion ([#845](https://github.com/standardnotes/syncing-server-js/issues/845)) ([0a1080c](https://github.com/standardnotes/syncing-server-js/commit/0a1080ce2a0fb021309a960de2c40193acab46eb))
|
||||
|
||||
# [1.104.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.103.1...@standardnotes/syncing-server@1.104.0) (2023-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** add designated survivors in fetching shared vaults response ([#844](https://github.com/standardnotes/syncing-server-js/issues/844)) ([bcd95cd](https://github.com/standardnotes/syncing-server-js/commit/bcd95cdbe9054d4ca39d5dc0486b6a0c0b6f52da))
|
||||
|
||||
## [1.103.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.103.0...@standardnotes/syncing-server@1.103.1) (2023-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable cleaning secondary database on transition ([4f4443a](https://github.com/standardnotes/syncing-server-js/commit/4f4443a882f69c2e76ef831ef36347c9c54f31cd))
|
||||
* integrity check during transition ([921c30f](https://github.com/standardnotes/syncing-server-js/commit/921c30f6415ef122a7d1af83ffa3f6840a42edba))
|
||||
* processing migration optimization ([22540ee](https://github.com/standardnotes/syncing-server-js/commit/22540ee83436b986949127a6923285a702162706))
|
||||
|
||||
# [1.103.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.102.2...@standardnotes/syncing-server@1.103.0) (2023-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* remove user from all shared vaults upon account deletion ([#843](https://github.com/standardnotes/syncing-server-js/issues/843)) ([dc77ff3](https://github.com/standardnotes/syncing-server-js/commit/dc77ff3e45983d231bc9c132802428e77b4be431))
|
||||
|
||||
## [1.102.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.102.1...@standardnotes/syncing-server@1.102.2) (2023-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** error message ([d0fd6b9](https://github.com/standardnotes/syncing-server-js/commit/d0fd6b98df58f6bd2050ff415515c692ecd32bef))
|
||||
|
||||
## [1.102.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.102.0...@standardnotes/syncing-server@1.102.1) (2023-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.102.1",
|
||||
"version": "1.105.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -169,6 +169,8 @@ import { DeleteSharedVaults } from '../Domain/UseCase/SharedVaults/DeleteSharedV
|
||||
import { RemoveItemsFromSharedVault } from '../Domain/UseCase/SharedVaults/RemoveItemsFromSharedVault/RemoveItemsFromSharedVault'
|
||||
import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler'
|
||||
import { DesignateSurvivor } from '../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor'
|
||||
import { RemoveUserFromSharedVaults } from '../Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults'
|
||||
import { TransferSharedVault } from '../Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVault'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -781,27 +783,6 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault)
|
||||
.toConstantValue(
|
||||
new DeleteSharedVault(
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<SharedVaultInviteRepositoryInterface>(TYPES.Sync_SharedVaultInviteRepository),
|
||||
container.get<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser),
|
||||
container.get<DeclineInviteToSharedVault>(TYPES.Sync_DeclineInviteToSharedVault),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteSharedVaults>(TYPES.Sync_DeleteSharedVaults)
|
||||
.toConstantValue(
|
||||
new DeleteSharedVaults(
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<CreateSharedVaultFileValetToken>(TYPES.Sync_CreateSharedVaultFileValetToken)
|
||||
.toConstantValue(
|
||||
@@ -870,12 +851,53 @@ 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),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<RemoveUserFromSharedVaults>(TYPES.Sync_RemoveUserFromSharedVaults)
|
||||
.toConstantValue(
|
||||
new RemoveUserFromSharedVaults(
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TransferSharedVault>(TYPES.Sync_TransferSharedVault)
|
||||
.toConstantValue(
|
||||
new TransferSharedVault(
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault)
|
||||
.toConstantValue(
|
||||
new DeleteSharedVault(
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<SharedVaultInviteRepositoryInterface>(TYPES.Sync_SharedVaultInviteRepository),
|
||||
container.get<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser),
|
||||
container.get<DeclineInviteToSharedVault>(TYPES.Sync_DeclineInviteToSharedVault),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<TransferSharedVault>(TYPES.Sync_TransferSharedVault),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteSharedVaults>(TYPES.Sync_DeleteSharedVaults)
|
||||
.toConstantValue(
|
||||
new DeleteSharedVaults(
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container
|
||||
@@ -938,6 +960,7 @@ export class ContainerConfigLoader {
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get<DeleteSharedVaults>(TYPES.Sync_DeleteSharedVaults),
|
||||
container.get<RemoveUserFromSharedVaults>(TYPES.Sync_RemoveUserFromSharedVaults),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -88,6 +88,8 @@ const TYPES = {
|
||||
Sync_SendEventToClient: Symbol.for('Sync_SendEventToClient'),
|
||||
Sync_RemoveItemsFromSharedVault: Symbol.for('Sync_RemoveItemsFromSharedVault'),
|
||||
Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'),
|
||||
Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'),
|
||||
Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
|
||||
@@ -4,11 +4,13 @@ import { Logger } from 'winston'
|
||||
|
||||
import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
|
||||
import { DeleteSharedVaults } from '../UseCase/SharedVaults/DeleteSharedVaults/DeleteSharedVaults'
|
||||
import { RemoveUserFromSharedVaults } from '../UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults'
|
||||
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private deleteSharedVaults: DeleteSharedVaults,
|
||||
private removeUserFromSharedVaults: RemoveUserFromSharedVaults,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -23,13 +25,24 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
|
||||
await itemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
|
||||
const result = await this.deleteSharedVaults.execute({
|
||||
const deletingVaultsResult = await this.deleteSharedVaults.execute({
|
||||
ownerUuid: event.payload.userUuid,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to delete shared vaults for user: ${event.payload.userUuid}: ${result.getError()}`)
|
||||
if (deletingVaultsResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to delete shared vaults for user: ${event.payload.userUuid}: ${deletingVaultsResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
const deletingUserFromOtherVaultsResult = await this.removeUserFromSharedVaults.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
if (deletingUserFromOtherVaultsResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to remove user: ${
|
||||
event.payload.userUuid
|
||||
} from shared vaults: ${deletingUserFromOtherVaultsResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUs
|
||||
import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
|
||||
import { SharedVaultInvite } from '../../../SharedVault/User/Invite/SharedVaultInvite'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { TransferSharedVault } from '../TransferSharedVault/TransferSharedVault'
|
||||
|
||||
describe('DeleteSharedVault', () => {
|
||||
let sharedVaultRepository: SharedVaultRepositoryInterface
|
||||
@@ -22,6 +23,7 @@ describe('DeleteSharedVault', () => {
|
||||
let sharedVaultInvite: SharedVaultInvite
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let transferSharedVault: TransferSharedVault
|
||||
|
||||
const createUseCase = () =>
|
||||
new DeleteSharedVault(
|
||||
@@ -32,9 +34,13 @@ describe('DeleteSharedVault', () => {
|
||||
declineInviteToSharedVault,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
transferSharedVault,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
transferSharedVault = {} as jest.Mocked<TransferSharedVault>
|
||||
transferSharedVault.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
sharedVault = SharedVault.create({
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
@@ -53,6 +59,7 @@ describe('DeleteSharedVault', () => {
|
||||
}).getValue()
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockResolvedValue([sharedVaultUser])
|
||||
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
sharedVaultInvite = SharedVaultInvite.create({
|
||||
encryptedMessage: 'test',
|
||||
@@ -171,7 +178,6 @@ describe('DeleteSharedVault', () => {
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).not.toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -187,6 +193,59 @@ describe('DeleteSharedVault', () => {
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('when shared vault has designated survivor', () => {
|
||||
beforeEach(() => {
|
||||
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
})
|
||||
|
||||
it('should transfer shared vault to designated survivor', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
|
||||
expect(transferSharedVault.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if transfering shared vault to designated survivor fails', async () => {
|
||||
transferSharedVault.execute = jest.fn().mockReturnValue(Result.fail('failed'))
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
|
||||
expect(transferSharedVault.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if removing owner from shared vault fails', async () => {
|
||||
removeUserFromSharedVault.execute = jest.fn().mockReturnValue(Result.fail('failed'))
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
|
||||
expect(transferSharedVault.execute).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SharedVaultInviteRepositoryInterface } from '../../../SharedVault/User/
|
||||
import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { TransferSharedVault } from '../TransferSharedVault/TransferSharedVault'
|
||||
|
||||
export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
@@ -18,6 +19,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
private declineInviteToSharedVault: DeclineInviteToSharedVault,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private transferSharedVault: TransferSharedVault,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeleteSharedVaultDTO): Promise<Result<void>> {
|
||||
@@ -42,13 +44,11 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
return Result.fail('Shared vault does not belong to the user')
|
||||
}
|
||||
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
for (const sharedVaultUser of sharedVaultUsers) {
|
||||
const result = await this.removeUserFromSharedVault.execute({
|
||||
originatorUuid: originatorUuid.value,
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
userUuid: sharedVaultUser.props.userUuid.value,
|
||||
forceRemoveOwner: true,
|
||||
const sharedVaultInvites = await this.sharedVaultInviteRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
for (const sharedVaultInvite of sharedVaultInvites) {
|
||||
const result = await this.declineInviteToSharedVault.execute({
|
||||
inviteUuid: sharedVaultInvite.id.toString(),
|
||||
userUuid: sharedVaultInvite.props.userUuid.value,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -56,11 +56,39 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const sharedVaultInvites = await this.sharedVaultInviteRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
for (const sharedVaultInvite of sharedVaultInvites) {
|
||||
const result = await this.declineInviteToSharedVault.execute({
|
||||
inviteUuid: sharedVaultInvite.id.toString(),
|
||||
userUuid: sharedVaultInvite.props.userUuid.value,
|
||||
const sharedVaultDesignatedSurvivor =
|
||||
await this.sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid)
|
||||
if (sharedVaultDesignatedSurvivor) {
|
||||
const result = await this.transferSharedVault.execute({
|
||||
sharedVaultUid: sharedVaultUuid.value,
|
||||
fromUserUuid: originatorUuid.value,
|
||||
toUserUuid: sharedVaultDesignatedSurvivor.props.userUuid.value,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
|
||||
const removingOwnerFromSharedVaultResult = await this.removeUserFromSharedVault.execute({
|
||||
originatorUuid: originatorUuid.value,
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
userUuid: originatorUuid.value,
|
||||
forceRemoveOwner: true,
|
||||
})
|
||||
if (removingOwnerFromSharedVaultResult.isFailed()) {
|
||||
return Result.fail(removingOwnerFromSharedVaultResult.getError())
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
for (const sharedVaultUser of sharedVaultUsers) {
|
||||
const result = await this.removeUserFromSharedVault.execute({
|
||||
originatorUuid: originatorUuid.value,
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
userUuid: sharedVaultUser.props.userUuid.value,
|
||||
forceRemoveOwner: true,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
export interface GetSharedVaultsDTO {
|
||||
userUuid: string
|
||||
includeDesignatedSurvivors?: boolean
|
||||
lastSyncTime?: number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { RemoveUserFromSharedVaults } from './RemoveUserFromSharedVaults'
|
||||
|
||||
describe('RemoveUserFromSharedVaults', () => {
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
let sharedVaultUser: SharedVaultUser
|
||||
let removeUserFromSharedVault: RemoveUserFromSharedVault
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new RemoveUserFromSharedVaults(sharedVaultUserRepository, removeUserFromSharedVault, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([sharedVaultUser])
|
||||
|
||||
removeUserFromSharedVault = {} as jest.Mocked<RemoveUserFromSharedVault>
|
||||
removeUserFromSharedVault.execute = jest.fn().mockResolvedValue(Result.ok())
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove user from shared vaults', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalledTimes(1)
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalledWith({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
forceRemoveOwner: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should log error if removing user from shared vault fails', async () => {
|
||||
removeUserFromSharedVault.execute = jest.fn().mockResolvedValue(Result.fail('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Failed to remove user: 00000000-0000-0000-0000-000000000000 from shared vault: 00000000-0000-0000-0000-000000000000: error',
|
||||
)
|
||||
})
|
||||
|
||||
it('should fail if the user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { Logger } from 'winston'
|
||||
import { RemoveUserFromSharedVaultsDTO } from './RemoveUserFromSharedVaultsDTO'
|
||||
|
||||
export class RemoveUserFromSharedVaults implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private removeUserFromSharedVault: RemoveUserFromSharedVault,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: RemoveUserFromSharedVaultsDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findByUserUuid(userUuid)
|
||||
for (const sharedVaultUser of sharedVaultUsers) {
|
||||
const result = await this.removeUserFromSharedVault.execute({
|
||||
sharedVaultUuid: sharedVaultUser.props.sharedVaultUuid.value,
|
||||
originatorUuid: userUuid.value,
|
||||
userUuid: userUuid.value,
|
||||
forceRemoveOwner: true,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to remove user: ${userUuid.value} from shared vault: ${
|
||||
sharedVaultUser.props.sharedVaultUuid.value
|
||||
}: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface RemoveUserFromSharedVaultsDTO {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { TransferSharedVault } from './TransferSharedVault'
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
|
||||
describe('TransferSharedVault', () => {
|
||||
let sharedVault: SharedVault
|
||||
let sharedVaultUser: SharedVaultUser
|
||||
let sharedVaultRepository: SharedVaultRepositoryInterface
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
let timer: TimerInterface
|
||||
|
||||
const createUseCase = () => new TransferSharedVault(sharedVaultRepository, sharedVaultUserRepository, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
sharedVault = SharedVault.create({
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
sharedVaultRepository.save = jest.fn()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
sharedVaultUserRepository.save = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
})
|
||||
|
||||
it('should transfer shared vault to another user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUid: '00000000-0000-0000-0000-000000000000',
|
||||
fromUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
toUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(sharedVaultRepository.save).toHaveBeenCalled()
|
||||
expect(sharedVaultUserRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if shared vault does not exist', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUid: '00000000-0000-0000-0000-000000000000',
|
||||
fromUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
toUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sharedVaultRepository.save).not.toHaveBeenCalled()
|
||||
expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if shared vault does not belong to user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
sharedVault.props.userUuid = Uuid.create('00000000-0000-0000-0000-000000000001').getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUid: '00000000-0000-0000-0000-000000000000',
|
||||
fromUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
toUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sharedVaultRepository.save).not.toHaveBeenCalled()
|
||||
expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if new owner is not a member of shared vault', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUid: '00000000-0000-0000-0000-000000000000',
|
||||
fromUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
toUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sharedVaultRepository.save).not.toHaveBeenCalled()
|
||||
expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if shared vault uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUid: 'invalid',
|
||||
fromUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
toUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sharedVaultRepository.save).not.toHaveBeenCalled()
|
||||
expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if from user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUid: '00000000-0000-0000-0000-000000000000',
|
||||
fromUserUuid: 'invalid',
|
||||
toUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sharedVaultRepository.save).not.toHaveBeenCalled()
|
||||
expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail if to user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUid: '00000000-0000-0000-0000-000000000000',
|
||||
fromUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
toUserUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sharedVaultRepository.save).not.toHaveBeenCalled()
|
||||
expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Result, SharedVaultUserPermission, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { TransferSharedVaultDTO } from './TransferSharedVaultDTO'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
|
||||
export class TransferSharedVault implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private sharedVaultRepository: SharedVaultRepositoryInterface,
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: TransferSharedVaultDTO): Promise<Result<void>> {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUid)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
|
||||
const fromUserUuidOrError = Uuid.create(dto.fromUserUuid)
|
||||
if (fromUserUuidOrError.isFailed()) {
|
||||
return Result.fail(fromUserUuidOrError.getError())
|
||||
}
|
||||
const fromUserUuid = fromUserUuidOrError.getValue()
|
||||
|
||||
const toUserUuidOrError = Uuid.create(dto.toUserUuid)
|
||||
if (toUserUuidOrError.isFailed()) {
|
||||
return Result.fail(toUserUuidOrError.getError())
|
||||
}
|
||||
const toUserUuid = toUserUuidOrError.getValue()
|
||||
|
||||
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
|
||||
if (!sharedVault) {
|
||||
return Result.fail('Shared vault not found')
|
||||
}
|
||||
|
||||
if (!sharedVault.props.userUuid.equals(fromUserUuid)) {
|
||||
return Result.fail('Shared vault does not belong to this user')
|
||||
}
|
||||
|
||||
const newOwner = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
|
||||
userUuid: toUserUuid,
|
||||
sharedVaultUuid: sharedVaultUuid,
|
||||
})
|
||||
if (!newOwner) {
|
||||
return Result.fail('New owner is not a member of this shared vault')
|
||||
}
|
||||
|
||||
newOwner.props.isDesignatedSurvivor = false
|
||||
newOwner.props.permission = SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Admin).getValue()
|
||||
newOwner.props.timestamps = Timestamps.create(
|
||||
newOwner.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
|
||||
await this.sharedVaultUserRepository.save(newOwner)
|
||||
|
||||
sharedVault.props.userUuid = toUserUuid
|
||||
sharedVault.props.timestamps = Timestamps.create(
|
||||
sharedVault.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
|
||||
await this.sharedVaultRepository.save(sharedVault)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface TransferSharedVaultDTO {
|
||||
sharedVaultUid: string
|
||||
fromUserUuid: string
|
||||
toUserUuid: string
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* istanbul ignore file */
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { TransitionItemsFromPrimaryToSecondaryDatabaseForUserDTO } from './TransitionItemsFromPrimaryToSecondaryDatabaseForUserDTO'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemQuery } from '../../../Item/ItemQuery'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Item } from '../../../Item/Item'
|
||||
|
||||
export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
|
||||
private readonly pagingProgress: Map<string, number> = new Map()
|
||||
|
||||
constructor(
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
@@ -30,73 +31,27 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
let newItemsInSecondaryCount = 0
|
||||
let updatedItemsInSecondary: string[] = []
|
||||
let alreadyIdenticalInSecondaryAndPrimary: string[] = []
|
||||
if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
|
||||
const { alreadyExistingInSecondaryAndPrimary, newItemsInSecondary, updatedInSecondary } =
|
||||
await this.getNewItemsCreatedInSecondaryDatabase(userUuid)
|
||||
|
||||
this.logger.info(
|
||||
`[${dto.userUuid}] ${alreadyExistingInSecondaryAndPrimary.length} already existing identical items in primary and secondary.`,
|
||||
)
|
||||
|
||||
alreadyIdenticalInSecondaryAndPrimary = alreadyExistingInSecondaryAndPrimary
|
||||
|
||||
if (newItemsInSecondary.length > 0) {
|
||||
this.logger.info(`[${dto.userUuid}] Found ${newItemsInSecondary.length} new items in secondary database.`)
|
||||
}
|
||||
|
||||
newItemsInSecondaryCount = newItemsInSecondary.length
|
||||
|
||||
if (updatedInSecondary.length > 0) {
|
||||
this.logger.info(`[${dto.userUuid}] Found ${updatedInSecondary.length} updated items in secondary database.`)
|
||||
}
|
||||
|
||||
updatedItemsInSecondary = updatedInSecondary
|
||||
}
|
||||
const updatedItemsInSecondaryCount = updatedItemsInSecondary.length
|
||||
|
||||
const migrationTimeStart = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Migrating items`)
|
||||
|
||||
const migrationResult = await this.migrateItemsForUser(
|
||||
userUuid,
|
||||
updatedItemsInSecondary,
|
||||
alreadyIdenticalInSecondaryAndPrimary,
|
||||
)
|
||||
const migrationResult = await this.migrateItemsForUser(userUuid)
|
||||
if (migrationResult.isFailed()) {
|
||||
if (newItemsInSecondaryCount === 0 && updatedItemsInSecondaryCount === 0) {
|
||||
const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
|
||||
if (cleanupResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Failed to clean up secondary database items: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.fail(migrationResult.getError())
|
||||
}
|
||||
const itemsToSkipInIntegrityCheck = migrationResult.getValue()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Items migrated`)
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Checking integrity between primary and secondary database`)
|
||||
|
||||
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
|
||||
userUuid,
|
||||
newItemsInSecondaryCount,
|
||||
updatedItemsInSecondary,
|
||||
alreadyIdenticalInSecondaryAndPrimary,
|
||||
itemsToSkipInIntegrityCheck,
|
||||
)
|
||||
if (integrityCheckResult.isFailed()) {
|
||||
if (newItemsInSecondaryCount === 0 && updatedItemsInSecondaryCount === 0) {
|
||||
const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
|
||||
if (cleanupResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Failed to clean up secondary database items: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.fail(integrityCheckResult.getError())
|
||||
}
|
||||
|
||||
@@ -117,109 +72,26 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async hasAlreadyDataInSecondaryDatabase(userUuid: Uuid): Promise<boolean> {
|
||||
const totalItemsCountForUser = await (this.secondaryItemRepository as ItemRepositoryInterface).countAll({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
|
||||
const hasAlreadyDataInSecondaryDatabase = totalItemsCountForUser > 0
|
||||
if (hasAlreadyDataInSecondaryDatabase) {
|
||||
this.logger.info(`[${userUuid.value}] User has already ${totalItemsCountForUser} items in secondary database`)
|
||||
}
|
||||
|
||||
return hasAlreadyDataInSecondaryDatabase
|
||||
}
|
||||
|
||||
private async allowForSecondaryDatabaseToCatchUp(): Promise<void> {
|
||||
const twoSecondsInMilliseconds = 2_000
|
||||
await this.timer.sleep(twoSecondsInMilliseconds)
|
||||
}
|
||||
|
||||
private async getNewItemsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
|
||||
alreadyExistingInSecondaryAndPrimary: string[]
|
||||
newItemsInSecondary: string[]
|
||||
updatedInSecondary: string[]
|
||||
}> {
|
||||
this.logger.info(`[${userUuid.value}] Checking for new items in secondary database`)
|
||||
|
||||
const alreadyExistingInSecondaryAndPrimary: string[] = []
|
||||
const updatedInSecondary: string[] = []
|
||||
const newItemsInSecondary: string[] = []
|
||||
|
||||
const totalItemsCountForUser = await (this.secondaryItemRepository as ItemRepositoryInterface).countAll({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
const totalPages = Math.ceil(totalItemsCountForUser / this.pageSize)
|
||||
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
|
||||
const query: ItemQuery = {
|
||||
userUuid: userUuid.value,
|
||||
offset: (currentPage - 1) * this.pageSize,
|
||||
limit: this.pageSize,
|
||||
sortOrder: 'ASC',
|
||||
sortBy: 'uuid',
|
||||
}
|
||||
|
||||
const items = await (this.secondaryItemRepository as ItemRepositoryInterface).findAll(query)
|
||||
for (const item of items) {
|
||||
const { identicalItemInPrimary, newerItemInSecondary } = await this.checkIfItemExistsInPrimaryDatabase(item)
|
||||
if (identicalItemInPrimary !== null) {
|
||||
alreadyExistingInSecondaryAndPrimary.push(item.id.toString())
|
||||
continue
|
||||
}
|
||||
if (newerItemInSecondary !== null) {
|
||||
updatedInSecondary.push(newerItemInSecondary.id.toString())
|
||||
continue
|
||||
}
|
||||
if (identicalItemInPrimary === null && newerItemInSecondary === null) {
|
||||
newItemsInSecondary.push(item.id.toString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
alreadyExistingInSecondaryAndPrimary,
|
||||
newItemsInSecondary,
|
||||
updatedInSecondary,
|
||||
}
|
||||
}
|
||||
|
||||
private async checkIfItemExistsInPrimaryDatabase(
|
||||
item: Item,
|
||||
): Promise<{ identicalItemInPrimary: Item | null; newerItemInSecondary: Item | null }> {
|
||||
const itemInPrimary = await this.primaryItemRepository.findByUuid(item.uuid)
|
||||
|
||||
if (itemInPrimary === null) {
|
||||
return { identicalItemInPrimary: null, newerItemInSecondary: null }
|
||||
}
|
||||
|
||||
if (!item.isIdenticalTo(itemInPrimary)) {
|
||||
this.logger.error(
|
||||
`[${
|
||||
item.props.userUuid.value
|
||||
}] Item ${item.id.toString()} is not identical in primary and secondary database. Item in secondary database: ${JSON.stringify(
|
||||
item,
|
||||
)}, item in primary database: ${JSON.stringify(itemInPrimary)}`,
|
||||
)
|
||||
|
||||
return {
|
||||
identicalItemInPrimary: null,
|
||||
newerItemInSecondary: item.props.timestamps.updatedAt > itemInPrimary.props.timestamps.updatedAt ? item : null,
|
||||
}
|
||||
}
|
||||
|
||||
return { identicalItemInPrimary: itemInPrimary, newerItemInSecondary: null }
|
||||
}
|
||||
|
||||
private async migrateItemsForUser(
|
||||
userUuid: Uuid,
|
||||
updatedItemsInSecondary: string[],
|
||||
alreadyExistingInSecondaryAndPrimary: string[],
|
||||
): Promise<Result<void>> {
|
||||
private async migrateItemsForUser(userUuid: Uuid): Promise<Result<string[]>> {
|
||||
try {
|
||||
if (!this.pagingProgress.has(userUuid.value)) {
|
||||
this.pagingProgress.set(userUuid.value, 1)
|
||||
}
|
||||
const initialPage = this.pagingProgress.get(userUuid.value) as number
|
||||
|
||||
this.logger.info(`[${userUuid.value}] Migrating from page ${initialPage}`)
|
||||
|
||||
const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
|
||||
const totalPages = Math.ceil(totalItemsCountForUser / this.pageSize)
|
||||
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
|
||||
const itemsToSkipInIntegrityCheck = []
|
||||
for (let currentPage = initialPage; currentPage <= totalPages; currentPage++) {
|
||||
this.pagingProgress.set(userUuid.value, currentPage)
|
||||
|
||||
const query: ItemQuery = {
|
||||
userUuid: userUuid.value,
|
||||
offset: (currentPage - 1) * this.pageSize,
|
||||
@@ -231,22 +103,41 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
const items = await this.primaryItemRepository.findAll(query)
|
||||
|
||||
for (const item of items) {
|
||||
if (updatedItemsInSecondary.find((updatedItemUuid) => item.uuid.value === updatedItemUuid)) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Skipping saving item ${item.uuid.value} as it was updated in secondary database`,
|
||||
try {
|
||||
const itemInSecondary = await (this.secondaryItemRepository as ItemRepositoryInterface).findByUuid(
|
||||
item.uuid,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
if (alreadyExistingInSecondaryAndPrimary.find((itemUuid) => item.uuid.value === itemUuid)) {
|
||||
continue
|
||||
}
|
||||
if (itemInSecondary !== null) {
|
||||
if (itemInSecondary.isIdenticalTo(item)) {
|
||||
continue
|
||||
}
|
||||
if (itemInSecondary.props.timestamps.updatedAt > item.props.timestamps.updatedAt) {
|
||||
this.logger.info(`[${userUuid.value}] Item ${item.uuid.value} is older than item in secondary database`)
|
||||
itemsToSkipInIntegrityCheck.push(item.uuid.value)
|
||||
|
||||
await (this.secondaryItemRepository as ItemRepositoryInterface).save(item)
|
||||
continue
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Removing item ${item.uuid.value} in secondary database as it is not identical to item in primary database`,
|
||||
)
|
||||
|
||||
await (this.secondaryItemRepository as ItemRepositoryInterface).removeByUuid(item.uuid)
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
}
|
||||
|
||||
await (this.secondaryItemRepository as ItemRepositoryInterface).save(item)
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Errored when saving item ${item.uuid.value} to secondary database: ${(error as Error).message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
return Result.ok(itemsToSkipInIntegrityCheck)
|
||||
} catch (error) {
|
||||
return Result.fail((error as Error).message)
|
||||
}
|
||||
@@ -254,6 +145,8 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
|
||||
private async deleteItemsForUser(userUuid: Uuid, itemRepository: ItemRepositoryInterface): Promise<Result<void>> {
|
||||
try {
|
||||
this.logger.info(`[${userUuid.value}] Cleaning up primary database items`)
|
||||
|
||||
await itemRepository.deleteByUserUuid(userUuid.value)
|
||||
|
||||
return Result.ok()
|
||||
@@ -264,9 +157,7 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
|
||||
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
|
||||
userUuid: Uuid,
|
||||
newItemsInSecondaryCount: number,
|
||||
updatedItemsInSecondary: string[],
|
||||
alreadyExistingInSecondaryAndPrimary: string[],
|
||||
itemsToSkipInIntegrityCheck: string[],
|
||||
): Promise<Result<boolean>> {
|
||||
try {
|
||||
const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
|
||||
@@ -276,9 +167,9 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
|
||||
if (totalItemsCountForUserInPrimary + newItemsInSecondaryCount !== totalItemsCountForUserInSecondary) {
|
||||
if (totalItemsCountForUserInPrimary > totalItemsCountForUserInSecondary) {
|
||||
return Result.fail(
|
||||
`Total items count for user ${userUuid.value} in primary database (${totalItemsCountForUserInPrimary} + ${newItemsInSecondaryCount}) does not match total items count in secondary database (${totalItemsCountForUserInSecondary})`,
|
||||
`Total items count for user ${userUuid.value} in primary database (${totalItemsCountForUserInPrimary}) does not match total items count in secondary database (${totalItemsCountForUserInSecondary})`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -300,14 +191,7 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
return Result.fail(`Item ${item.uuid.value} not found in secondary database`)
|
||||
}
|
||||
|
||||
if (updatedItemsInSecondary.find((updatedItemUuid) => item.uuid.value === updatedItemUuid)) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Skipping integrity check for item ${item.uuid.value} as it was updated in secondary database`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (alreadyExistingInSecondaryAndPrimary.find((itemUuid) => item.uuid.value === itemUuid)) {
|
||||
if (itemsToSkipInIntegrityCheck.includes(item.id.toString())) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -20,7 +20,7 @@ export class MessagePersistenceMapper implements MapperInterface<Message, TypeOR
|
||||
|
||||
const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
|
||||
if (timestampsOrError.isFailed()) {
|
||||
throw new Error(`Failed to create notification from projection: ${timestampsOrError.getError()}`)
|
||||
throw new Error(`Failed to create message from projection: ${timestampsOrError.getError()}`)
|
||||
}
|
||||
const timestamps = timestampsOrError.getValue()
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.50](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.49...@standardnotes/websockets-server@1.10.50) (2023-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.10.49](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.48...@standardnotes/websockets-server@1.10.49) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.10.49",
|
||||
"version": "1.10.50",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user