mirror of
https://github.com/standardnotes/server
synced 2026-02-15 20:01:13 -05:00
Compare commits
19 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
951d965304 | ||
|
|
29e8de3238 | ||
|
|
eeeacabaa8 | ||
|
|
51ca8229b8 | ||
|
|
a6a19a391e | ||
|
|
f6cdb7916c | ||
|
|
eafb064d79 | ||
|
|
ba050681f7 | ||
|
|
4780629549 | ||
|
|
79a44aa51f | ||
|
|
dd72769841 | ||
|
|
d8f1c66fd5 | ||
|
|
afe9967d26 | ||
|
|
27bea444cc | ||
|
|
7a571dec0a | ||
|
|
8c57f505be | ||
|
|
973612bf4f | ||
|
|
702a1286eb | ||
|
|
a45b5b69b5 |
@@ -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.28.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.28.0...@standardnotes/analytics@2.28.1) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add xray-sdk to background processors ([a45b5b6](https://github.com/standardnotes/server/commit/a45b5b69b5d68c2e696c30f0ba5ad22d313321e6))
|
||||
|
||||
# [2.28.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.27.10...@standardnotes/analytics@2.28.0) (2023-10-06)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import * as AWSXRay from 'aws-xray-sdk'
|
||||
|
||||
import { EmailLevel } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
@@ -253,6 +254,14 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForAWSProduction =
|
||||
env.get('MODE', true) !== 'home-server' && env.get('MODE', true) !== 'self-hosted'
|
||||
|
||||
if (isConfiguredForAWSProduction) {
|
||||
AWSXRay.enableManualMode()
|
||||
AWSXRay.config([AWSXRay.plugins.ECSPlugin])
|
||||
}
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting usage report generation...')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.28.0",
|
||||
"version": "2.28.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.152.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.152.0...@standardnotes/auth-server@1.152.1) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** checking for transition role when triggering transition ([79a44aa](https://github.com/standardnotes/server/commit/79a44aa51f15311fcaf76c39f93d1934ec1d135d))
|
||||
|
||||
# [1.152.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.151.2...@standardnotes/auth-server@1.152.0) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** strip user from transition role after migration ([afe9967](https://github.com/standardnotes/server/commit/afe9967d26b5be02d1dc76a740df614d81a6984e))
|
||||
|
||||
### Features
|
||||
|
||||
* switch transition direction ([27bea44](https://github.com/standardnotes/server/commit/27bea444cce4964feda04bad64e5f12a07415e0c))
|
||||
|
||||
## [1.151.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.151.1...@standardnotes/auth-server@1.151.2) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add xray-sdk to background processors ([a45b5b6](https://github.com/standardnotes/server/commit/a45b5b69b5d68c2e696c30f0ba5ad22d313321e6))
|
||||
|
||||
## [1.151.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.151.0...@standardnotes/auth-server@1.151.1) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Stream } from 'stream'
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
import * as AWSXRay from 'aws-xray-sdk'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
@@ -82,6 +83,14 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForAWSProduction =
|
||||
env.get('MODE', true) !== 'home-server' && env.get('MODE', true) !== 'self-hosted'
|
||||
|
||||
if (isConfiguredForAWSProduction) {
|
||||
AWSXRay.enableManualMode()
|
||||
AWSXRay.config([AWSXRay.plugins.ECSPlugin])
|
||||
}
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info(`Starting ${backupFrequency} ${backupProvider} backup requesting...`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import * as AWSXRay from 'aws-xray-sdk'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
@@ -23,6 +24,14 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForAWSProduction =
|
||||
env.get('MODE', true) !== 'home-server' && env.get('MODE', true) !== 'self-hosted'
|
||||
|
||||
if (isConfiguredForAWSProduction) {
|
||||
AWSXRay.enableManualMode()
|
||||
AWSXRay.config([AWSXRay.plugins.ECSPlugin])
|
||||
}
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info('Starting sessions and session traces cleanup')
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import * as AWSXRay from 'aws-xray-sdk'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
@@ -13,6 +14,14 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForAWSProduction =
|
||||
env.get('MODE', true) !== 'home-server' && env.get('MODE', true) !== 'self-hosted'
|
||||
|
||||
if (isConfiguredForAWSProduction) {
|
||||
AWSXRay.enableManualMode()
|
||||
AWSXRay.config([AWSXRay.plugins.ECSPlugin])
|
||||
}
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info('Starting session traces cleanup')
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
import * as AWSXRay from 'aws-xray-sdk'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
@@ -49,7 +50,7 @@ const requestTransition = async (
|
||||
itemsTransitionStatus?.value === TransitionStatus.STATUSES.Verified &&
|
||||
revisionsTransitionStatus?.value === TransitionStatus.STATUSES.Verified
|
||||
|
||||
if (userHasTransitionRole && bothTransitionStatusesAreVerified) {
|
||||
if (!userHasTransitionRole && bothTransitionStatusesAreVerified) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -102,6 +103,14 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForAWSProduction =
|
||||
env.get('MODE', true) !== 'home-server' && env.get('MODE', true) !== 'self-hosted'
|
||||
|
||||
if (isConfiguredForAWSProduction) {
|
||||
AWSXRay.enableManualMode()
|
||||
AWSXRay.config([AWSXRay.plugins.ECSPlugin])
|
||||
}
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info(`Starting transition request for users created between ${startDateString} and ${endDateString}`)
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
import * as AWSXRay from 'aws-xray-sdk'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
@@ -70,6 +71,14 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForAWSProduction =
|
||||
env.get('MODE', true) !== 'home-server' && env.get('MODE', true) !== 'self-hosted'
|
||||
|
||||
if (isConfiguredForAWSProduction) {
|
||||
AWSXRay.enableManualMode()
|
||||
AWSXRay.config([AWSXRay.plugins.ECSPlugin])
|
||||
}
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info(`Starting email backup requesting for ${backupEmail} ...`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.151.1",
|
||||
"version": "1.152.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -118,6 +118,40 @@ describe('RoleService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('removing roles', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([basicRole]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
it('should remove a role from a user', async () => {
|
||||
await createService().removeRoleFromUser(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
|
||||
)
|
||||
|
||||
user.roles = Promise.resolve([])
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user)
|
||||
})
|
||||
|
||||
it('should not remove a role from a user if the user could not be found', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createService().removeRoleFromUser(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
|
||||
)
|
||||
|
||||
expect(userRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('adding roles based on subscription', () => {
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
|
||||
@@ -65,6 +65,17 @@ export class RoleService implements RoleServiceInterface {
|
||||
await this.addToExistingRoles(user, roleName.value)
|
||||
}
|
||||
|
||||
async removeRoleFromUser(userUuid: Uuid, roleName: RoleName): Promise<void> {
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.error(`Could not find user with uuid ${userUuid.value} to remove role ${roleName.value}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.removeUserRole(user, roleName.value)
|
||||
}
|
||||
|
||||
async addUserRoleBasedOnSubscription(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
|
||||
|
||||
@@ -108,9 +119,15 @@ export class RoleService implements RoleServiceInterface {
|
||||
return
|
||||
}
|
||||
|
||||
await this.removeUserRole(user, roleName)
|
||||
}
|
||||
|
||||
private async removeUserRole(user: User, roleName: string): Promise<void> {
|
||||
const currentRoles = await user.roles
|
||||
user.roles = Promise.resolve(currentRoles.filter((role) => role.name !== roleName))
|
||||
|
||||
await this.userRepository.save(user)
|
||||
|
||||
await this.webSocketsClientService.sendUserRolesChangedEvent(user)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { User } from '../User/User'
|
||||
|
||||
export interface RoleServiceInterface {
|
||||
addRoleToUser(userUuid: Uuid, roleName: RoleName): Promise<void>
|
||||
removeRoleFromUser(userUuid: Uuid, roleName: RoleName): Promise<void>
|
||||
addUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
|
||||
setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void>
|
||||
removeUserRoleBasedOnSubscription(user: User, subscriptionName: string): Promise<void>
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('UpdateTransitionStatus', () => {
|
||||
transitionStatusRepository.getStatus = jest.fn().mockResolvedValue(null)
|
||||
|
||||
roleService = {} as jest.Mocked<RoleServiceInterface>
|
||||
roleService.addRoleToUser = jest.fn()
|
||||
roleService.removeRoleFromUser = jest.fn()
|
||||
})
|
||||
|
||||
it('should add TRANSITION_USER role', async () => {
|
||||
@@ -35,7 +35,7 @@ describe('UpdateTransitionStatus', () => {
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(roleService.addRoleToUser).toHaveBeenCalledWith(
|
||||
expect(roleService.removeRoleFromUser).toHaveBeenCalledWith(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ export class UpdateTransitionStatus implements UseCaseInterface<void> {
|
||||
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, transitionStatus)
|
||||
|
||||
if (dto.transitionType === 'items' && transitionStatus.value === TransitionStatus.STATUSES.Verified) {
|
||||
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
|
||||
await this.roleService.removeRoleFromUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/Tra
|
||||
import { TransitionStatus } from '@standardnotes/domain-core'
|
||||
|
||||
export class RedisTransitionStatusRepository implements TransitionStatusRepositoryInterface {
|
||||
private readonly PREFIX = 'transition'
|
||||
private readonly PREFIX = 'transition-back'
|
||||
|
||||
constructor(private redisClient: IORedis.Redis) {}
|
||||
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.16.35](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.34...@standardnotes/home-server@1.16.35) (2023-10-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.34](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.33...@standardnotes/home-server@1.16.34) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.33](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.32...@standardnotes/home-server@1.16.33) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.32](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.31...@standardnotes/home-server@1.16.32) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.31](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.30...@standardnotes/home-server@1.16.31) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.30](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.29...@standardnotes/home-server@1.16.30) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.16.29](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.16.28...@standardnotes/home-server@1.16.29) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.16.29",
|
||||
"version": "1.16.35",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.40.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.40.1...@standardnotes/revisions-server@1.40.2) (2023-10-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* logs in transition ([29e8de3](https://github.com/standardnotes/server/commit/29e8de32383e911bbb431d3fd0da68faefa32d3d))
|
||||
|
||||
## [1.40.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.40.0...@standardnotes/revisions-server@1.40.1) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** casting creation date from MongoDB ([4780629](https://github.com/standardnotes/server/commit/47806295491867ca5fd53e39757f057a0722ae28))
|
||||
|
||||
# [1.40.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.39.5...@standardnotes/revisions-server@1.40.0) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable TransitionRequestedEventHandler ([d8f1c66](https://github.com/standardnotes/server/commit/d8f1c66fd5e59285ccaa1be36da2ee9796b81ccb))
|
||||
|
||||
### Features
|
||||
|
||||
* switch transition direction ([27bea44](https://github.com/standardnotes/server/commit/27bea444cce4964feda04bad64e5f12a07415e0c))
|
||||
|
||||
## [1.39.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.39.4...@standardnotes/revisions-server@1.39.5) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.39.5",
|
||||
"version": "1.40.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -268,7 +268,7 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(new MongoDBRevisionMetadataPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<Revision, MongoDBRevision>>(TYPES.Revisions_MongoDBRevisionPersistenceMapper)
|
||||
.toConstantValue(new MongoDBRevisionPersistenceMapper())
|
||||
.toConstantValue(new MongoDBRevisionPersistenceMapper(container.get<TimerInterface>(TYPES.Revisions_Timer)))
|
||||
|
||||
// ORM
|
||||
container
|
||||
@@ -491,7 +491,7 @@ export class ContainerConfigLoader {
|
||||
.bind<TransitionRequestedEventHandler>(TYPES.Revisions_TransitionRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new TransitionRequestedEventHandler(
|
||||
true,
|
||||
false,
|
||||
container.get<TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser>(
|
||||
TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
|
||||
),
|
||||
|
||||
@@ -61,7 +61,7 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Revisions migrated`)
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
await this.allowForPrimaryDatabaseToCatchUp()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Checking integrity between primary and secondary database`)
|
||||
|
||||
@@ -75,11 +75,16 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
return Result.fail(integrityCheckResult.getError())
|
||||
}
|
||||
|
||||
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.primaryRevisionsRepository)
|
||||
const cleanupResult = await this.deleteRevisionsForUser(
|
||||
userUuid,
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface,
|
||||
)
|
||||
if (cleanupResult.isFailed()) {
|
||||
await this.updateTransitionStatus(userUuid, TransitionStatus.STATUSES.Failed, dto.timestamp)
|
||||
|
||||
this.logger.error(`[${dto.userUuid}] Failed to clean up primary database revisions: ${cleanupResult.getError()}`)
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Failed to clean up secondary database revisions: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const migrationTimeEnd = this.timer.getTimestampInMicroseconds()
|
||||
@@ -104,7 +109,9 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
|
||||
this.logger.info(`[${userUuid.value}] Migrating from page ${initialPage}`)
|
||||
|
||||
const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
const totalRevisionsCountForUser = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).countByUserUuid(userUuid)
|
||||
const totalPages = Math.ceil(totalRevisionsCountForUser / this.pageSize)
|
||||
for (let currentPage = initialPage; currentPage <= totalPages; currentPage++) {
|
||||
const isPageInEvery10Percent = currentPage % Math.ceil(totalPages / 10) === 0
|
||||
@@ -128,47 +135,49 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
limit: this.pageSize,
|
||||
}
|
||||
|
||||
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
|
||||
const revisions = await (this.secondRevisionsRepository as RevisionRepositoryInterface).findByUserUuid(query)
|
||||
for (const revision of revisions) {
|
||||
try {
|
||||
const revisionInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).findOneByUuid(Uuid.create(revision.id.toString()).getValue(), revision.props.userUuid as Uuid, [])
|
||||
const revisionInPrimary = await this.primaryRevisionsRepository.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) {
|
||||
if (revisionInPrimary !== null) {
|
||||
if (revisionInPrimary.props.dates.updatedAt > revision.props.dates.updatedAt) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Revision ${revision.id.toString()} is older than revision in secondary database`,
|
||||
`[${
|
||||
userUuid.value
|
||||
}] Revision ${revision.id.toString()} is older in secondary than revision in primary database`,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
if (revisionInPrimary.isIdenticalTo(revision)) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[${
|
||||
userUuid.value
|
||||
}] Removing revision ${revision.id.toString()} in secondary database as it is not identical to revision in primary database`,
|
||||
`[${userUuid.value}] Removing revision ${revision.id.toString()} in primary database: ${JSON.stringify(
|
||||
revisionInPrimary,
|
||||
)} as it is not identical to revision in secondary database: ${JSON.stringify(revision)}`,
|
||||
)
|
||||
|
||||
await (this.secondRevisionsRepository as RevisionRepositoryInterface).removeOneByUuid(
|
||||
Uuid.create(revisionInSecondary.id.toString()).getValue(),
|
||||
revisionInSecondary.props.userUuid as Uuid,
|
||||
await this.primaryRevisionsRepository.removeOneByUuid(
|
||||
Uuid.create(revisionInPrimary.id.toString()).getValue(),
|
||||
revisionInPrimary.props.userUuid as Uuid,
|
||||
)
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
await this.allowForPrimaryDatabaseToCatchUp()
|
||||
}
|
||||
|
||||
const didSave = await (this.secondRevisionsRepository as RevisionRepositoryInterface).insert(revision)
|
||||
const didSave = await this.primaryRevisionsRepository.insert(revision)
|
||||
if (!didSave) {
|
||||
this.logger.error(`Failed to save revision ${revision.id.toString()} to secondary database`)
|
||||
this.logger.error(`Failed to save revision ${revision.id.toString()} to primary database`)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Errored when saving revision ${revision.id.toString()} to secondary database: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
`Errored when saving revision ${revision.id.toString()} to primary database: ${(error as Error).message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -185,7 +194,7 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
revisionRepository: RevisionRepositoryInterface,
|
||||
): Promise<Result<void>> {
|
||||
try {
|
||||
this.logger.info(`[${userUuid.value}] Deleting all revisions from primary database`)
|
||||
this.logger.info(`[${userUuid.value}] Deleting all revisions from secondary database`)
|
||||
|
||||
await revisionRepository.removeByUserUuid(userUuid)
|
||||
|
||||
@@ -195,9 +204,9 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
}
|
||||
|
||||
private async allowForSecondaryDatabaseToCatchUp(): Promise<void> {
|
||||
const twoSecondsInMilliseconds = 2_000
|
||||
await this.timer.sleep(twoSecondsInMilliseconds)
|
||||
private async allowForPrimaryDatabaseToCatchUp(): Promise<void> {
|
||||
const delay = 1_000
|
||||
await this.timer.sleep(delay)
|
||||
}
|
||||
|
||||
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid: Uuid): Promise<Result<boolean>> {
|
||||
@@ -208,12 +217,12 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
|
||||
this.logger.info(`[${userUuid.value}] Checking integrity from page ${initialPage}`)
|
||||
|
||||
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
const totalRevisionsCountForUserInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).countByUserUuid(userUuid)
|
||||
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
|
||||
if (totalRevisionsCountForUserInPrimary > totalRevisionsCountForUserInSecondary) {
|
||||
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})`,
|
||||
)
|
||||
@@ -232,7 +241,7 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
limit: this.pageSize,
|
||||
}
|
||||
|
||||
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
|
||||
const revisions = await (this.secondRevisionsRepository as RevisionRepositoryInterface).findByUserUuid(query)
|
||||
|
||||
for (const revision of revisions) {
|
||||
const revisionUuidOrError = Uuid.create(revision.id.toString())
|
||||
@@ -242,31 +251,29 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
const revisionUuid = revisionUuidOrError.getValue()
|
||||
|
||||
const revisionInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).findOneByUuid(revisionUuid, userUuid, [])
|
||||
if (!revisionInSecondary) {
|
||||
return Result.fail(`Revision ${revision.id.toString()} not found in secondary database`)
|
||||
const revisionInPrimary = await this.primaryRevisionsRepository.findOneByUuid(revisionUuid, userUuid, [])
|
||||
if (!revisionInPrimary) {
|
||||
return Result.fail(`Revision ${revision.id.toString()} not found in primary database`)
|
||||
}
|
||||
|
||||
if (revision.isIdenticalTo(revisionInSecondary)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (revisionInSecondary.props.dates.updatedAt > revision.props.dates.updatedAt) {
|
||||
if (revisionInPrimary.props.dates.updatedAt > revision.props.dates.updatedAt) {
|
||||
this.logger.info(
|
||||
`[${
|
||||
userUuid.value
|
||||
}] Integrity check of revision ${revision.id.toString()} - is older than revision in secondary database`,
|
||||
}] Integrity check of revision ${revision.id.toString()} - is older in secondary than revision in primary database`,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (revision.isIdenticalTo(revisionInPrimary)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return Result.fail(
|
||||
`Revision ${revision.id.toString()} is not identical in primary and secondary database. Revision in primary database: ${JSON.stringify(
|
||||
revision,
|
||||
)}, revision in secondary database: ${JSON.stringify(revisionInSecondary)}`,
|
||||
revisionInPrimary,
|
||||
)}, revision in secondary database: ${JSON.stringify(revision)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -291,14 +298,16 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
|
||||
private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> {
|
||||
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
const totalRevisionsCountForUserInSecondary = await (
|
||||
this.secondRevisionsRepository as RevisionRepositoryInterface
|
||||
).countByUserUuid(userUuid)
|
||||
|
||||
if (totalRevisionsCountForUserInPrimary > 0) {
|
||||
if (totalRevisionsCountForUserInSecondary > 0) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] User has ${totalRevisionsCountForUserInPrimary} revisions in primary database.`,
|
||||
`[${userUuid.value}] User has ${totalRevisionsCountForUserInSecondary} revisions in secondary database.`,
|
||||
)
|
||||
}
|
||||
|
||||
return totalRevisionsCountForUserInPrimary === 0
|
||||
return totalRevisionsCountForUserInSecondary === 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import { MongoDBRevision } from '../../../Infra/TypeORM/MongoDB/MongoDBRevision'
|
||||
import { Revision } from '../../../Domain/Revision/Revision'
|
||||
import { SharedVaultAssociation } from '../../../Domain/SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../../Domain/KeySystem/KeySystemAssociation'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
export class MongoDBRevisionPersistenceMapper implements MapperInterface<Revision, MongoDBRevision> {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
toDomain(projection: MongoDBRevision): Revision {
|
||||
const contentTypeOrError = ContentType.create(projection.contentType)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
@@ -73,7 +76,7 @@ export class MongoDBRevisionPersistenceMapper implements MapperInterface<Revisio
|
||||
authHash: projection.authHash,
|
||||
content: projection.content,
|
||||
contentType,
|
||||
creationDate: projection.creationDate,
|
||||
creationDate: new Date(this.timer.convertDateToFormattedString(projection.creationDate, 'YYYY-MM-DD')),
|
||||
encItemKey: projection.encItemKey,
|
||||
itemsKeyId: projection.itemsKeyId,
|
||||
itemUuid,
|
||||
@@ -99,7 +102,9 @@ export class MongoDBRevisionPersistenceMapper implements MapperInterface<Revisio
|
||||
mongoDBRevision.contentType = domain.props.contentType.value
|
||||
mongoDBRevision.createdAt = domain.props.dates.createdAt
|
||||
mongoDBRevision.updatedAt = domain.props.dates.updatedAt
|
||||
mongoDBRevision.creationDate = domain.props.creationDate
|
||||
mongoDBRevision.creationDate = new Date(
|
||||
this.timer.convertDateToFormattedString(domain.props.creationDate, 'YYYY-MM-DD'),
|
||||
)
|
||||
mongoDBRevision.encItemKey = domain.props.encItemKey
|
||||
mongoDBRevision.itemUuid = domain.props.itemUuid.value
|
||||
mongoDBRevision.itemsKeyId = domain.props.itemsKeyId
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [1.22.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.22.0...@standardnotes/scheduler-server@1.22.1) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add xray-sdk to background processors ([a45b5b6](https://github.com/standardnotes/server/commit/a45b5b69b5d68c2e696c30f0ba5ad22d313321e6))
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.21.10...@standardnotes/scheduler-server@1.22.0) (2023-10-06)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
import * as AWSXRay from 'aws-xray-sdk'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
@@ -21,6 +22,14 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForAWSProduction =
|
||||
env.get('MODE', true) !== 'home-server' && env.get('MODE', true) !== 'self-hosted'
|
||||
|
||||
if (isConfiguredForAWSProduction) {
|
||||
AWSXRay.enableManualMode()
|
||||
AWSXRay.config([AWSXRay.plugins.ECSPlugin])
|
||||
}
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
const timer: TimerInterface = container.get(TYPES.Timer)
|
||||
const now = timer.getTimestampInMicroseconds()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.22.0",
|
||||
"version": "1.22.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,48 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.112.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.112.2...@standardnotes/syncing-server@1.112.3) (2023-10-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* logs in transition ([29e8de3](https://github.com/standardnotes/syncing-server-js/commit/29e8de32383e911bbb431d3fd0da68faefa32d3d))
|
||||
|
||||
## [1.112.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.112.1...@standardnotes/syncing-server@1.112.2) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** calling auth server for user key params ([51ca822](https://github.com/standardnotes/syncing-server-js/commit/51ca8229b8d5ebb3b4573a2a9da12dd8f15bf2ec))
|
||||
* **syncing-server:** error log on email backup requested ([a6a19a3](https://github.com/standardnotes/syncing-server-js/commit/a6a19a391e0495a0f362b98d0f3a34e4f6539863))
|
||||
|
||||
## [1.112.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.112.0...@standardnotes/syncing-server@1.112.1) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** increase axios timeout on calling auth ([eafb064](https://github.com/standardnotes/syncing-server-js/commit/eafb064d7992dc8aa31f090e4265498c415c5795))
|
||||
* **syncing-server:** logs on request backup handler ([ba05068](https://github.com/standardnotes/syncing-server-js/commit/ba050681f772c2f566462be57f6b0731141d85b0))
|
||||
|
||||
# [1.112.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.111.5...@standardnotes/syncing-server@1.112.0) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable TransitionRequestedEventHandler ([d8f1c66](https://github.com/standardnotes/syncing-server-js/commit/d8f1c66fd5e59285ccaa1be36da2ee9796b81ccb))
|
||||
|
||||
### Features
|
||||
|
||||
* switch transition direction ([27bea44](https://github.com/standardnotes/syncing-server-js/commit/27bea444cce4964feda04bad64e5f12a07415e0c))
|
||||
|
||||
## [1.111.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.111.4...@standardnotes/syncing-server@1.111.5) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** add more logs on successfull email backups requested ([8c57f50](https://github.com/standardnotes/syncing-server-js/commit/8c57f505be86f3a7af0ab446a409bac276b2242b))
|
||||
|
||||
## [1.111.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.111.3...@standardnotes/syncing-server@1.111.4) (2023-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** error log on email backup request handler ([702a128](https://github.com/standardnotes/syncing-server-js/commit/702a1286eb5ef9414dc64fb91afbefa98b007cf3))
|
||||
|
||||
## [1.111.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.111.2...@standardnotes/syncing-server@1.111.3) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.111.3",
|
||||
"version": "1.112.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1066,7 +1066,7 @@ export class ContainerConfigLoader {
|
||||
.bind<TransitionRequestedEventHandler>(TYPES.Sync_TransitionRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new TransitionRequestedEventHandler(
|
||||
true,
|
||||
false,
|
||||
container.get<TransitionItemsFromPrimaryToSecondaryDatabaseForUser>(
|
||||
TYPES.Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
|
||||
export interface AuthHttpServiceInterface {
|
||||
getUserKeyParams(dto: { email?: string; uuid?: string; authenticated: boolean }): Promise<KeyParamsData>
|
||||
getUserKeyParams(userUuid: string): Promise<KeyParamsData>
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
DuplicateItemSyncedEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { Item } from '../Item/Item'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { DuplicateItemSyncedEventHandler } from './DuplicateItemSyncedEventHandler'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('DuplicateItemSyncedEventHandler', () => {
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let logger: Logger
|
||||
let duplicateItem: Item
|
||||
let originalItem: Item
|
||||
let event: DuplicateItemSyncedEvent
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new DuplicateItemSyncedEventHandler(itemRepositoryResolver, domainEventFactory, domainEventPublisher, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
originalItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
duplicateItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuidAndUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(duplicateItem)
|
||||
.mockReturnValueOnce(originalItem)
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
logger.debug = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<DuplicateItemSyncedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
itemUuid: '2-3-4',
|
||||
roleNames: ['CORE_USER'],
|
||||
}
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createRevisionsCopyRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<RevisionsCopyRequestedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should copy revisions from original item to the duplicate item', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should do nothing if role names are not valid', async () => {
|
||||
event.payload.roleNames = ['INVALID_ROLE_NAME']
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not copy revisions if original item does not exist', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not copy revisions if duplicate item does not exist', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not copy revisions if duplicate item is not pointing to duplicate anything', async () => {
|
||||
duplicateItem.props.duplicateOf = null
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,137 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
EmailBackupRequestedEvent,
|
||||
EmailRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { AuthHttpServiceInterface } from '../Auth/AuthHttpServiceInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { Item } from '../Item/Item'
|
||||
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { EmailBackupRequestedEventHandler } from './EmailBackupRequestedEventHandler'
|
||||
import { ItemTransferCalculatorInterface } from '../Item/ItemTransferCalculatorInterface'
|
||||
import { ItemContentSizeDescriptor } from '../Item/ItemContentSizeDescriptor'
|
||||
|
||||
describe('EmailBackupRequestedEventHandler', () => {
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let authHttpService: AuthHttpServiceInterface
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
const emailAttachmentMaxByteSize = 100
|
||||
let itemTransferCalculator: ItemTransferCalculatorInterface
|
||||
let item: Item
|
||||
let event: EmailBackupRequestedEvent
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new EmailBackupRequestedEventHandler(
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
authHttpService,
|
||||
itemBackupService,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
emailAttachmentMaxByteSize,
|
||||
itemTransferCalculator,
|
||||
's3-backup-bucket-name',
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
item = {} as jest.Mocked<Item>
|
||||
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
primaryItemRepository.findContentSizeForComputingTransferLimit = jest
|
||||
.fn()
|
||||
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
|
||||
|
||||
authHttpService = {} as jest.Mocked<AuthHttpServiceInterface>
|
||||
authHttpService.getUserKeyParams = jest.fn().mockReturnValue({ identifier: 'test@test.com' })
|
||||
|
||||
event = {} as jest.Mocked<EmailBackupRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
userHasEmailsMuted: false,
|
||||
muteEmailsSettingUuid: '1-2-3',
|
||||
}
|
||||
|
||||
itemBackupService = {} as jest.Mocked<ItemBackupServiceInterface>
|
||||
itemBackupService.backup = jest.fn().mockReturnValue(['backup-file-name'])
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
|
||||
|
||||
itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
|
||||
itemTransferCalculator.computeItemUuidBundlesToFetch = jest.fn().mockReturnValue([['1-2-3']])
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should inform that backup attachment for email was created', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should inform that backup attachment for email was created in the secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
secondaryItemRepository.findContentSizeForComputingTransferLimit = jest
|
||||
.fn()
|
||||
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalledTimes(2)
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should inform that multipart backup attachment for email was created', async () => {
|
||||
itemBackupService.backup = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(['backup-file-name-1'])
|
||||
.mockReturnValueOnce(['backup-file-name-2', 'backup-file-name-3'])
|
||||
itemTransferCalculator.computeItemUuidBundlesToFetch = jest.fn().mockReturnValue([['1-2-3'], ['2-3-4']])
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(3)
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should not inform that backup attachment for email was created if user key params cannot be obtained', async () => {
|
||||
authHttpService.getUserKeyParams = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops!')
|
||||
})
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not inform that backup attachment for email was created if backup file name is empty', async () => {
|
||||
itemBackupService.backup = jest.fn().mockReturnValue('')
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -42,12 +42,13 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
): Promise<void> {
|
||||
let authParams: KeyParamsData
|
||||
try {
|
||||
authParams = await this.authHttpService.getUserKeyParams({
|
||||
uuid: event.payload.userUuid,
|
||||
authenticated: false,
|
||||
})
|
||||
authParams = await this.authHttpService.getUserKeyParams(event.payload.userUuid)
|
||||
} catch (error) {
|
||||
this.logger.warn(`Could not get user key params from auth service: ${(error as Error).message}`)
|
||||
this.logger.error(
|
||||
`Could not get user key params from auth service for user ${event.payload.userUuid}: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -104,5 +105,7 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
this.logger.info(`Email with backup requested for user ${event.payload.userUuid}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Items migrated`)
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
await this.allowForPrimaryDatabaseToCatchUp()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Checking integrity between primary and secondary database`)
|
||||
|
||||
@@ -74,11 +74,14 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
return Result.fail(integrityCheckResult.getError())
|
||||
}
|
||||
|
||||
const cleanupResult = await this.deleteItemsForUser(userUuid, this.primaryItemRepository)
|
||||
const cleanupResult = await this.deleteItemsForUser(
|
||||
userUuid,
|
||||
this.secondaryItemRepository as ItemRepositoryInterface,
|
||||
)
|
||||
if (cleanupResult.isFailed()) {
|
||||
await this.updateTransitionStatus(userUuid, TransitionStatus.STATUSES.Failed, dto.timestamp)
|
||||
|
||||
this.logger.error(`[${dto.userUuid}] Failed to clean up primary database items: ${cleanupResult.getError()}`)
|
||||
this.logger.error(`[${dto.userUuid}] Failed to clean up secondary database items: ${cleanupResult.getError()}`)
|
||||
}
|
||||
|
||||
const migrationTimeEnd = this.timer.getTimestampInMicroseconds()
|
||||
@@ -95,9 +98,9 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async allowForSecondaryDatabaseToCatchUp(): Promise<void> {
|
||||
const twoSecondsInMilliseconds = 2_000
|
||||
await this.timer.sleep(twoSecondsInMilliseconds)
|
||||
private async allowForPrimaryDatabaseToCatchUp(): Promise<void> {
|
||||
const delay = 1_000
|
||||
await this.timer.sleep(delay)
|
||||
}
|
||||
|
||||
private async migrateItemsForUser(userUuid: Uuid, timestamp: number): Promise<Result<void>> {
|
||||
@@ -108,7 +111,9 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
|
||||
this.logger.info(`[${userUuid.value}] Migrating from page ${initialPage}`)
|
||||
|
||||
const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
|
||||
const totalItemsCountForUser = await (this.secondaryItemRepository as ItemRepositoryInterface).countAll({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
const totalPages = Math.ceil(totalItemsCountForUser / this.pageSize)
|
||||
for (let currentPage = initialPage; currentPage <= totalPages; currentPage++) {
|
||||
const isPageInEvery10Percent = currentPage % Math.ceil(totalPages / 10) === 0
|
||||
@@ -132,37 +137,37 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
sortOrder: 'ASC',
|
||||
}
|
||||
|
||||
const items = await this.primaryItemRepository.findAll(query)
|
||||
const items = await (this.secondaryItemRepository as ItemRepositoryInterface).findAll(query)
|
||||
|
||||
for (const item of items) {
|
||||
try {
|
||||
const itemInSecondary = await (this.secondaryItemRepository as ItemRepositoryInterface).findByUuid(
|
||||
item.uuid,
|
||||
)
|
||||
const itemInPrimary = await this.primaryItemRepository.findByUuid(item.uuid)
|
||||
|
||||
if (itemInPrimary !== null) {
|
||||
if (itemInPrimary.props.timestamps.updatedAt > item.props.timestamps.updatedAt) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Item ${item.uuid.value} is older in secondary than item in primary database`,
|
||||
)
|
||||
|
||||
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`)
|
||||
|
||||
if (itemInPrimary.isIdenticalTo(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`,
|
||||
`[${userUuid.value}] Removing item ${item.uuid.value} in primary database as it is not identical to item in primary database`,
|
||||
)
|
||||
|
||||
await (this.secondaryItemRepository as ItemRepositoryInterface).removeByUuid(item.uuid)
|
||||
await this.primaryItemRepository.removeByUuid(item.uuid)
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
await this.allowForPrimaryDatabaseToCatchUp()
|
||||
}
|
||||
|
||||
await (this.secondaryItemRepository as ItemRepositoryInterface).save(item)
|
||||
await this.primaryItemRepository.save(item)
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Errored when saving item ${item.uuid.value} to secondary database: ${(error as Error).message}`,
|
||||
`Errored when saving item ${item.uuid.value} to primary database: ${(error as Error).message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -194,14 +199,16 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
|
||||
this.logger.info(`[${userUuid.value}] Checking integrity from page ${initialPage}`)
|
||||
|
||||
const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
|
||||
const totalItemsCountForUserInSecondary = await (
|
||||
this.secondaryItemRepository as ItemRepositoryInterface
|
||||
).countAll({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
|
||||
if (totalItemsCountForUserInPrimary > totalItemsCountForUserInSecondary) {
|
||||
if (totalItemsCountForUserInPrimary < totalItemsCountForUserInSecondary) {
|
||||
return Result.fail(
|
||||
`Total items count for user ${userUuid.value} in primary database (${totalItemsCountForUserInPrimary}) does not match total items count in secondary database (${totalItemsCountForUserInSecondary})`,
|
||||
)
|
||||
@@ -222,32 +229,32 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
sortOrder: 'ASC',
|
||||
}
|
||||
|
||||
const items = await this.primaryItemRepository.findAll(query)
|
||||
const items = await (this.secondaryItemRepository as ItemRepositoryInterface).findAll(query)
|
||||
|
||||
for (const item of items) {
|
||||
const itemInSecondary = await (this.secondaryItemRepository as ItemRepositoryInterface).findByUuid(item.uuid)
|
||||
if (!itemInSecondary) {
|
||||
return Result.fail(`Item ${item.uuid.value} not found in secondary database`)
|
||||
const itemInPrimary = await this.primaryItemRepository.findByUuid(item.uuid)
|
||||
if (!itemInPrimary) {
|
||||
return Result.fail(`Item ${item.uuid.value} not found in primary database`)
|
||||
}
|
||||
|
||||
if (item.isIdenticalTo(itemInSecondary)) {
|
||||
if (itemInPrimary.props.timestamps.updatedAt > item.props.timestamps.updatedAt) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Integrity check of Item ${item.uuid.value} - is older in secondary than item in primary database`,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (itemInSecondary.props.timestamps.updatedAt > item.props.timestamps.updatedAt) {
|
||||
this.logger.info(
|
||||
`[${userUuid.value}] Integrity check of Item ${item.uuid.value} - is older than item in secondary database`,
|
||||
)
|
||||
|
||||
if (item.isIdenticalTo(itemInPrimary)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return Result.fail(
|
||||
`Item ${
|
||||
item.uuid.value
|
||||
} is not identical in primary and secondary database. Item in primary database: ${JSON.stringify(
|
||||
} is not identical in primary and secondary database. Item in secondary database: ${JSON.stringify(
|
||||
item,
|
||||
)}, item in secondary database: ${JSON.stringify(itemInSecondary)}`,
|
||||
)}, item in primary database: ${JSON.stringify(itemInPrimary)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -270,14 +277,14 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
}
|
||||
|
||||
private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> {
|
||||
const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({
|
||||
const totalItemsCountForUserInSecondary = await (this.secondaryItemRepository as ItemRepositoryInterface).countAll({
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
|
||||
if (totalItemsCountForUserInPrimary > 0) {
|
||||
this.logger.info(`[${userUuid.value}] User has ${totalItemsCountForUserInPrimary} items in primary database.`)
|
||||
if (totalItemsCountForUserInSecondary > 0) {
|
||||
this.logger.info(`[${userUuid.value}] User has ${totalItemsCountForUserInSecondary} items in secondary database.`)
|
||||
}
|
||||
|
||||
return totalItemsCountForUserInPrimary === 0
|
||||
return totalItemsCountForUserInSecondary === 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { AxiosInstance } from 'axios'
|
||||
|
||||
import { AuthHttpService } from './AuthHttpService'
|
||||
|
||||
describe('AuthHttpService', () => {
|
||||
let httpClient: AxiosInstance
|
||||
|
||||
const authServerUrl = 'https://auth-server'
|
||||
|
||||
const createService = () => new AuthHttpService(httpClient, authServerUrl)
|
||||
|
||||
beforeEach(() => {
|
||||
httpClient = {} as jest.Mocked<AxiosInstance>
|
||||
httpClient.request = jest.fn().mockReturnValue({ data: { foo: 'bar' } })
|
||||
})
|
||||
|
||||
it('should send a request to auth service in order to get user key params', async () => {
|
||||
await createService().getUserKeyParams({
|
||||
email: 'test@test.com',
|
||||
authenticated: false,
|
||||
})
|
||||
|
||||
expect(httpClient.request).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
url: 'https://auth-server/users/params',
|
||||
params: {
|
||||
authenticated: false,
|
||||
email: 'test@test.com',
|
||||
},
|
||||
validateStatus: expect.any(Function),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -9,14 +9,14 @@ export class AuthHttpService implements AuthHttpServiceInterface {
|
||||
private authServerUrl: string,
|
||||
) {}
|
||||
|
||||
async getUserKeyParams(dto: { email?: string; uuid?: string; authenticated: boolean }): Promise<KeyParamsData> {
|
||||
async getUserKeyParams(userUuid: string): Promise<KeyParamsData> {
|
||||
const keyParamsResponse = await this.httpClient.request({
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
url: `${this.authServerUrl}/users/params`,
|
||||
params: dto,
|
||||
url: `${this.authServerUrl}/users/params?uuid=${userUuid}`,
|
||||
validateStatus:
|
||||
/* istanbul ignore next */
|
||||
(status: number) => status >= 200 && status < 500,
|
||||
|
||||
Reference in New Issue
Block a user