mirror of
https://github.com/standardnotes/server
synced 2026-02-04 14:01:16 -05:00
Compare commits
28 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94448bb5d8 | ||
|
|
9a568b0f73 | ||
|
|
a1ee491dc5 | ||
|
|
e5c118c262 | ||
|
|
1bef1279e6 | ||
|
|
c511f259c7 | ||
|
|
f77ed8ef94 | ||
|
|
a4929af2ee | ||
|
|
095811dda9 | ||
|
|
480d5879ba | ||
|
|
c0722b173b | ||
|
|
f07c8e4bd4 | ||
|
|
baf4b2c1d2 | ||
|
|
a6039bd99a | ||
|
|
7c0010c902 | ||
|
|
596a0f1a02 | ||
|
|
efda3df09b | ||
|
|
ec35f46d45 | ||
|
|
c64fa2f47c | ||
|
|
6ce42a0101 | ||
|
|
d40c74c072 | ||
|
|
d722206916 | ||
|
|
19e4c8bf5e | ||
|
|
ee656b868b | ||
|
|
5e79d28bbf | ||
|
|
25ffd6b803 | ||
|
|
a08fe8087f | ||
|
|
fe273a9107 |
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -30,6 +30,11 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/domain-core"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/domain-events"
|
||||
schedule:
|
||||
@@ -50,6 +55,11 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/home-server"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/packages/predicates"
|
||||
schedule:
|
||||
|
||||
1
.pnp.cjs
generated
1
.pnp.cjs
generated
@@ -5861,6 +5861,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
||||
["@aws-sdk/client-s3", "npm:3.342.0"],\
|
||||
["@aws-sdk/client-sns", "npm:3.342.0"],\
|
||||
["@aws-sdk/client-sqs", "npm:3.342.0"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
|
||||
@@ -122,6 +122,10 @@ echo "linking topic $FILES_TOPIC_ARN to queue $AUTH_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $FILES_TOPIC_ARN $AUTH_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
echo "linking topic $REVISIONS_TOPIC_ARN to queue $AUTH_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $REVISIONS_TOPIC_ARN $AUTH_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
QUEUE_NAME="files-local-queue"
|
||||
|
||||
@@ -174,6 +178,11 @@ LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $REVISIONS_QUEUE
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
echo "linking topic $REVISIONS_TOPIC_ARN to queue $REVISIONS_QUEUE_ARN"
|
||||
LINKING_RESULT=$(link_queue_and_topic $REVISIONS_TOPIC_ARN $REVISIONS_QUEUE_ARN)
|
||||
echo "linking done:"
|
||||
echo "$LINKING_RESULT"
|
||||
|
||||
QUEUE_NAME="scheduler-local-queue"
|
||||
|
||||
echo "creating queue $QUEUE_NAME"
|
||||
|
||||
@@ -3,6 +3,29 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.26.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.21...@standardnotes/analytics@2.26.0) (2023-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** throwing errors on unexisting users ([c511f25](https://github.com/standardnotes/server/commit/c511f259c765fe5cb5b022213d2a59d67390a3c4))
|
||||
* remove the alive and kicking info logs on workers ([1bef127](https://github.com/standardnotes/server/commit/1bef1279e6dbf3cbdfa87e44aa9108ed6dbb3b0f))
|
||||
|
||||
### Features
|
||||
|
||||
* send websocket event to user when a message is sent ([#802](https://github.com/standardnotes/server/issues/802)) ([9a568b0](https://github.com/standardnotes/server/commit/9a568b0f73078ab74d4771bac469903a124e67da))
|
||||
|
||||
## [2.25.21](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.20...@standardnotes/analytics@2.25.21) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.19...@standardnotes/analytics@2.25.20) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.19](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.18...@standardnotes/analytics@2.25.19) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.18](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.17...@standardnotes/analytics@2.25.18) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -22,6 +22,4 @@ void container.load().then((container) => {
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.18",
|
||||
"version": "2.26.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -41,13 +41,13 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.analyticsEntityRepository.remove(analyticsEntity)
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
distinct_id: analyticsEntity.id.toString(),
|
||||
user_created_at: this.timer.convertMicrosecondsToDate(event.payload.userCreatedAtTimestamp),
|
||||
})
|
||||
}
|
||||
|
||||
await this.analyticsEntityRepository.remove(analyticsEntity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentFailedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentFailed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -88,7 +88,11 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentSuccessEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentSuccess], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -17,8 +17,11 @@ export class RefundProcessedEventHandler implements DomainEventHandlerInterface
|
||||
) {}
|
||||
|
||||
async handle(event: RefundProcessedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId } = analyticsMetadataOrError.getValue()
|
||||
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -13,7 +13,11 @@ export class SessionCreatedEventHandler implements DomainEventHandlerInterface {
|
||||
) {}
|
||||
|
||||
async handle(event: SessionCreatedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: event.payload.userUuid })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userUuid: event.payload.userUuid })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId } = analyticsMetadataOrError.getValue()
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
|
||||
@@ -13,7 +13,11 @@ export class SessionRefreshedEventHandler implements DomainEventHandlerInterface
|
||||
) {}
|
||||
|
||||
async handle(event: SessionRefreshedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: event.payload.userUuid })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userUuid: event.payload.userUuid })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId } = analyticsMetadataOrError.getValue()
|
||||
|
||||
if (this.mixpanelClient !== null) {
|
||||
this.mixpanelClient.track(event.type, {
|
||||
|
||||
@@ -29,7 +29,11 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId, userUuid } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionCancelled], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -27,7 +27,11 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId, userUuid } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity(
|
||||
[AnalyticsActivity.SubscriptionExpired, AnalyticsActivity.ExistingCustomersChurn],
|
||||
analyticsId,
|
||||
|
||||
@@ -29,7 +29,11 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId, userUuid } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionPurchased], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -19,7 +19,11 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionReactivated], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -27,7 +27,11 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId, userUuid } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionRefunded], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -26,7 +26,11 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
const analyticsMetadataOrError = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
if (analyticsMetadataOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const { analyticsId, userUuid } = analyticsMetadataOrError.getValue()
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionRenewed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
|
||||
@@ -24,23 +24,18 @@ describe('GetUserAnalyticsId', () => {
|
||||
})
|
||||
|
||||
it('should return analytics id for a user by uuid', async () => {
|
||||
expect((await createUseCase().execute({ userUuid: '1-2-3' })).analyticsId).toEqual(123)
|
||||
expect((await createUseCase().execute({ userUuid: '1-2-3' })).getValue().analyticsId).toEqual(123)
|
||||
})
|
||||
|
||||
it('should return analytics id for a user by email', async () => {
|
||||
expect((await createUseCase().execute({ userEmail: 'test@test.te' })).analyticsId).toEqual(123)
|
||||
expect((await createUseCase().execute({ userEmail: 'test@test.te' })).getValue().analyticsId).toEqual(123)
|
||||
})
|
||||
|
||||
it('should throw error if user is missing analytics entity', async () => {
|
||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
let error = null
|
||||
|
||||
try {
|
||||
await createUseCase().execute({ userUuid: '1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3' })
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
expect(result.isFailed()).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Username, Uuid } from '@standardnotes/domain-core'
|
||||
import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../../Entity/AnalyticsEntityRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { GetUserAnalyticsIdDTO } from './GetUserAnalyticsIdDTO'
|
||||
import { GetUserAnalyticsIdResponse } from './GetUserAnalyticsIdResponse'
|
||||
|
||||
@injectable()
|
||||
export class GetUserAnalyticsId implements UseCaseInterface {
|
||||
export class GetUserAnalyticsId implements UseCaseInterface<GetUserAnalyticsIdResponse> {
|
||||
constructor(
|
||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetUserAnalyticsIdDTO): Promise<GetUserAnalyticsIdResponse> {
|
||||
async execute(dto: GetUserAnalyticsIdDTO): Promise<Result<GetUserAnalyticsIdResponse>> {
|
||||
let analyticsEntity = null
|
||||
if (dto.userUuid) {
|
||||
analyticsEntity = await this.analyticsEntityRepository.findOneByUserUuid(dto.userUuid)
|
||||
@@ -22,13 +21,13 @@ export class GetUserAnalyticsId implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (analyticsEntity === null) {
|
||||
throw new Error(`Could not find analytics entity for user ${dto.userUuid}`)
|
||||
return Result.fail(`Could not find analytics entity ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
return {
|
||||
return Result.ok({
|
||||
analyticsId: analyticsEntity.id,
|
||||
userUuid: Uuid.create(analyticsEntity.userUuid).getValue(),
|
||||
username: Username.create(analyticsEntity.username).getValue(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.73.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.73.3...@standardnotes/api-gateway@1.73.4) (2023-09-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.73.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.73.2...@standardnotes/api-gateway@1.73.3) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.73.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.73.1...@standardnotes/api-gateway@1.73.2) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.73.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.73.0...@standardnotes/api-gateway@1.73.1) (2023-08-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** transition triggering endpoint call for revisions ([ee656b8](https://github.com/standardnotes/api-gateway/commit/ee656b868b8ebcd63b568b48a450803c80fa78a6))
|
||||
|
||||
# [1.73.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.72.3...@standardnotes/api-gateway@1.73.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
* add a way to trigger transition procedure for revisions ([#798](https://github.com/standardnotes/api-gateway/issues/798)) ([25ffd6b](https://github.com/standardnotes/api-gateway/commit/25ffd6b8036117b33584c6d948bb0867b637ae65))
|
||||
|
||||
## [1.72.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.72.2...@standardnotes/api-gateway@1.72.3) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.72.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.72.1...@standardnotes/api-gateway@1.72.2) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.72.2",
|
||||
"version": "1.73.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import { TYPES } from '../../Bootstrap/Types'
|
||||
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
|
||||
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
|
||||
|
||||
@controller('/v2/items/:itemUuid/revisions', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
@controller('/v2', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
|
||||
export class RevisionsControllerV2 extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.ApiGateway_ServiceProxy) private httpService: ServiceProxyInterface,
|
||||
@inject(TYPES.ApiGateway_ServiceProxy) private serviceProxy: ServiceProxyInterface,
|
||||
@inject(TYPES.ApiGateway_EndpointResolver) private endpointResolver: EndpointResolverInterface,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
@httpGet('/items/:itemUuid/revisions')
|
||||
async getRevisions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callRevisionsServer(
|
||||
await this.serviceProxy.callRevisionsServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier(
|
||||
@@ -28,9 +28,9 @@ export class RevisionsControllerV2 extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:uuid')
|
||||
@httpGet('/items/:itemUuid/revisions/:uuid')
|
||||
async getRevision(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callRevisionsServer(
|
||||
await this.serviceProxy.callRevisionsServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier(
|
||||
@@ -42,9 +42,9 @@ export class RevisionsControllerV2 extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
@httpDelete('/:uuid')
|
||||
@httpDelete('/items/:itemUuid/revisions/:uuid')
|
||||
async deleteRevision(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callRevisionsServer(
|
||||
await this.serviceProxy.callRevisionsServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier(
|
||||
@@ -55,4 +55,14 @@ export class RevisionsControllerV2 extends BaseHttpController {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/revisions/transition')
|
||||
async transition(request: Request, response: Response): Promise<void> {
|
||||
await this.serviceProxy.callRevisionsServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('POST', 'revisions/transition'),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],
|
||||
['[GET]:items/:itemUuid/revisions/:id', 'revisions.revisions.getRevision'],
|
||||
['[DELETE]:items/:itemUuid/revisions/:id', 'revisions.revisions.deleteRevision'],
|
||||
['[POST]:revisions/transition', 'revisions.revisions.transition'],
|
||||
// Messages Controller
|
||||
['[GET]:messages/', 'sync.messages.get-received'],
|
||||
['[GET]:messages/outbound', 'sync.messages.get-sent'],
|
||||
|
||||
@@ -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.137.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.137.2...@standardnotes/auth-server@1.137.3) (2023-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove the alive and kicking info logs on workers ([1bef127](https://github.com/standardnotes/server/commit/1bef1279e6dbf3cbdfa87e44aa9108ed6dbb3b0f))
|
||||
|
||||
## [1.137.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.137.1...@standardnotes/auth-server@1.137.2) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.137.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.137.0...@standardnotes/auth-server@1.137.1) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.137.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.136.0...@standardnotes/auth-server@1.137.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add procedure for transitioning data from primary to secondary database ([#787](https://github.com/standardnotes/server/issues/787)) ([fe273a9](https://github.com/standardnotes/server/commit/fe273a9107585b5953c8de1d0f8afb951f891bca))
|
||||
|
||||
# [1.136.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.2...@standardnotes/auth-server@1.136.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -24,6 +24,4 @@ void container.load().then((container) => {
|
||||
TYPES.Auth_DomainEventSubscriberFactory,
|
||||
)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.136.0",
|
||||
"version": "1.137.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
|
||||
const result = await this.updateTransitionStatusUseCase.execute({
|
||||
status: event.payload.status,
|
||||
userUuid: event.payload.userUuid,
|
||||
transitionType: event.payload.transitionType,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface TransitionStatusRepositoryInterface {
|
||||
updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void>
|
||||
removeStatus(userUuid: string): Promise<void>
|
||||
getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null>
|
||||
updateStatus(userUuid: string, transitionType: 'items' | 'revisions', status: 'STARTED' | 'FAILED'): Promise<void>
|
||||
removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void>
|
||||
getStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<'STARTED' | 'FAILED' | null>
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
|
||||
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(user.uuid)
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(user.uuid, 'items')
|
||||
|
||||
const roles = await user.roles
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('GetTransitionStatus', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -52,6 +53,7 @@ describe('GetTransitionStatus', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -63,6 +65,7 @@ describe('GetTransitionStatus', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -76,6 +79,7 @@ describe('GetTransitionStatus', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -87,6 +91,7 @@ describe('GetTransitionStatus', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -100,6 +105,7 @@ describe('GetTransitionStatus', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
@@ -29,7 +29,7 @@ export class GetTransitionStatus implements UseCaseInterface<'TO-DO' | 'STARTED'
|
||||
}
|
||||
}
|
||||
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid.value)
|
||||
const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid.value, dto.transitionType)
|
||||
if (transitionStatus === null) {
|
||||
return Result.ok('TO-DO')
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export interface GetTransitionStatusDTO {
|
||||
userUuid: string
|
||||
transitionType: 'items' | 'revisions'
|
||||
}
|
||||
|
||||
@@ -25,10 +25,14 @@ describe('UpdateTransitionStatus', () => {
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
status: 'FINISHED',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith('00000000-0000-0000-0000-000000000000')
|
||||
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'items',
|
||||
)
|
||||
expect(roleService.addRoleToUser).toHaveBeenCalledWith(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
|
||||
@@ -41,11 +45,13 @@ describe('UpdateTransitionStatus', () => {
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
status: 'STARTED',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(transitionStatusRepository.updateStatus).toHaveBeenCalledWith(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'items',
|
||||
'STARTED',
|
||||
)
|
||||
})
|
||||
@@ -56,6 +62,7 @@ describe('UpdateTransitionStatus', () => {
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
status: 'STARTED',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
@@ -17,14 +17,14 @@ export class UpdateTransitionStatus implements UseCaseInterface<void> {
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
if (dto.status === 'FINISHED') {
|
||||
await this.transitionStatusRepository.removeStatus(dto.userUuid)
|
||||
await this.transitionStatusRepository.removeStatus(dto.userUuid, dto.transitionType)
|
||||
|
||||
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.status)
|
||||
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface UpdateTransitionStatusDTO {
|
||||
userUuid: string
|
||||
transitionType: 'items' | 'revisions'
|
||||
status: 'STARTED' | 'FINISHED' | 'FAILED'
|
||||
}
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
import { TransitionStatusRepositoryInterface } from '../../Domain/Transition/TransitionStatusRepositoryInterface'
|
||||
|
||||
export class InMemoryTransitionStatusRepository implements TransitionStatusRepositoryInterface {
|
||||
private statuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
|
||||
private itemStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
|
||||
private revisionStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
|
||||
|
||||
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
|
||||
this.statuses.set(userUuid, status)
|
||||
async updateStatus(
|
||||
userUuid: string,
|
||||
transitionType: 'items' | 'revisions',
|
||||
status: 'STARTED' | 'FAILED',
|
||||
): Promise<void> {
|
||||
if (transitionType === 'items') {
|
||||
this.itemStatuses.set(userUuid, status)
|
||||
} else {
|
||||
this.revisionStatuses.set(userUuid, status)
|
||||
}
|
||||
}
|
||||
|
||||
async removeStatus(userUuid: string): Promise<void> {
|
||||
this.statuses.delete(userUuid)
|
||||
async removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void> {
|
||||
if (transitionType === 'items') {
|
||||
this.itemStatuses.delete(userUuid)
|
||||
} else {
|
||||
this.revisionStatuses.delete(userUuid)
|
||||
}
|
||||
}
|
||||
|
||||
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
|
||||
const status = this.statuses.get(userUuid) || null
|
||||
async getStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<'STARTED' | 'FAILED' | null> {
|
||||
let status: 'STARTED' | 'FAILED' | null = null
|
||||
|
||||
if (transitionType === 'items') {
|
||||
status = this.itemStatuses.get(userUuid) ?? null
|
||||
} else {
|
||||
status = this.revisionStatuses.get(userUuid) ?? null
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ export class BaseUsersController extends BaseHttpController {
|
||||
async transitionStatus(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.getTransitionStatusUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -7,16 +7,23 @@ export class RedisTransitionStatusRepository implements TransitionStatusReposito
|
||||
|
||||
constructor(private redisClient: IORedis.Redis) {}
|
||||
|
||||
async updateStatus(userUuid: string, status: 'STARTED' | 'FAILED'): Promise<void> {
|
||||
await this.redisClient.set(`${this.PREFIX}:${userUuid}`, status)
|
||||
async updateStatus(
|
||||
userUuid: string,
|
||||
transitionType: 'items' | 'revisions',
|
||||
status: 'STARTED' | 'FAILED',
|
||||
): Promise<void> {
|
||||
await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status)
|
||||
}
|
||||
|
||||
async removeStatus(userUuid: string): Promise<void> {
|
||||
await this.redisClient.del(`${this.PREFIX}:${userUuid}`)
|
||||
async removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void> {
|
||||
await this.redisClient.del(`${this.PREFIX}:${transitionType}:${userUuid}`)
|
||||
}
|
||||
|
||||
async getStatus(userUuid: string): Promise<'STARTED' | 'FAILED' | null> {
|
||||
const status = (await this.redisClient.get(`${this.PREFIX}:${userUuid}`)) as 'STARTED' | 'FAILED' | null
|
||||
async getStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<'STARTED' | 'FAILED' | null> {
|
||||
const status = (await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)) as
|
||||
| 'STARTED'
|
||||
| 'FAILED'
|
||||
| null
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.18](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.17...@standardnotes/domain-events-infra@1.12.18) (2023-09-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.17](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.16...@standardnotes/domain-events-infra@1.12.17) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.16](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.15...@standardnotes/domain-events-infra@1.12.16) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.15](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.14...@standardnotes/domain-events-infra@1.12.15) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.15",
|
||||
"version": "1.12.18",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.121.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.120.0...@standardnotes/domain-events@2.121.0) (2023-09-01)
|
||||
|
||||
### Features
|
||||
|
||||
* send websocket event to user when a message is sent ([#802](https://github.com/standardnotes/server/issues/802)) ([9a568b0](https://github.com/standardnotes/server/commit/9a568b0f73078ab74d4771bac469903a124e67da))
|
||||
|
||||
# [2.120.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.119.0...@standardnotes/domain-events@2.120.0) (2023-08-31)
|
||||
|
||||
### Features
|
||||
|
||||
* add sending notifications to user via websockets ([#799](https://github.com/standardnotes/server/issues/799)) ([c0722b1](https://github.com/standardnotes/server/commit/c0722b173b71d696568d8e8c5095a22fd219bef6))
|
||||
|
||||
# [2.119.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.118.0...@standardnotes/domain-events@2.119.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add procedure for transitioning data from primary to secondary database ([#787](https://github.com/standardnotes/server/issues/787)) ([fe273a9](https://github.com/standardnotes/server/commit/fe273a9107585b5953c8de1d0f8afb951f891bca))
|
||||
|
||||
# [2.118.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.117.0...@standardnotes/domain-events@2.118.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.118.0",
|
||||
"version": "2.121.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { MessageSentToUserEventPayload } from './MessageSentToUserEventPayload'
|
||||
|
||||
export interface MessageSentToUserEvent extends DomainEventInterface {
|
||||
type: 'MESSAGE_SENT_TO_USER'
|
||||
payload: MessageSentToUserEventPayload
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface MessageSentToUserEventPayload {
|
||||
message: {
|
||||
uuid: string
|
||||
recipient_uuid: string
|
||||
sender_uuid: string
|
||||
encrypted_message: string
|
||||
replaceability_identifier: string | null
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
|
||||
import { NotificationAddedForUserEventPayload } from './NotificationAddedForUserEventPayload'
|
||||
|
||||
export interface NotificationAddedForUserEvent extends DomainEventInterface {
|
||||
type: 'NOTIFICATION_ADDED_FOR_USER'
|
||||
payload: NotificationAddedForUserEventPayload
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface NotificationAddedForUserEventPayload {
|
||||
notification: {
|
||||
uuid: string
|
||||
user_uuid: string
|
||||
type: string
|
||||
payload: string
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface TransitionStatusUpdatedEventPayload {
|
||||
userUuid: string
|
||||
transitionType: 'items' | 'revisions'
|
||||
status: 'STARTED' | 'FINISHED' | 'FAILED'
|
||||
}
|
||||
|
||||
@@ -40,8 +40,12 @@ export * from './Event/ListedAccountDeletedEvent'
|
||||
export * from './Event/ListedAccountDeletedEventPayload'
|
||||
export * from './Event/ListedAccountRequestedEvent'
|
||||
export * from './Event/ListedAccountRequestedEventPayload'
|
||||
export * from './Event/MessageSentToUserEvent'
|
||||
export * from './Event/MessageSentToUserEventPayload'
|
||||
export * from './Event/MuteEmailsSettingChangedEvent'
|
||||
export * from './Event/MuteEmailsSettingChangedEventPayload'
|
||||
export * from './Event/NotificationAddedForUserEvent'
|
||||
export * from './Event/NotificationAddedForUserEventPayload'
|
||||
export * from './Event/PaymentFailedEvent'
|
||||
export * from './Event/PaymentFailedEventPayload'
|
||||
export * from './Event/PaymentsAccountDeletedEvent'
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.28](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.27...@standardnotes/event-store@1.11.28) (2023-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove the alive and kicking info logs on workers ([1bef127](https://github.com/standardnotes/server/commit/1bef1279e6dbf3cbdfa87e44aa9108ed6dbb3b0f))
|
||||
|
||||
## [1.11.27](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.26...@standardnotes/event-store@1.11.27) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.26](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.25...@standardnotes/event-store@1.11.26) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.25](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.24...@standardnotes/event-store@1.11.25) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.24](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.23...@standardnotes/event-store@1.11.24) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -18,6 +18,4 @@ void container.load().then((container) => {
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.24",
|
||||
"version": "1.11.28",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.7](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.6...@standardnotes/files-server@1.22.7) (2023-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove the alive and kicking info logs on workers ([1bef127](https://github.com/standardnotes/files/commit/1bef1279e6dbf3cbdfa87e44aa9108ed6dbb3b0f))
|
||||
|
||||
## [1.22.6](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.5...@standardnotes/files-server@1.22.6) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.22.5](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.4...@standardnotes/files-server@1.22.5) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.22.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.3...@standardnotes/files-server@1.22.4) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.22.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.2...@standardnotes/files-server@1.22.3) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -24,6 +24,4 @@ void container.load().then((container) => {
|
||||
TYPES.Files_DomainEventSubscriberFactory,
|
||||
)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.22.3",
|
||||
"version": "1.22.7",
|
||||
"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.15.18](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.17...@standardnotes/home-server@1.15.18) (2023-09-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.17](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.16...@standardnotes/home-server@1.15.17) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.16](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.15...@standardnotes/home-server@1.15.16) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.15](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.14...@standardnotes/home-server@1.15.15) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.14](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.13...@standardnotes/home-server@1.15.14) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.13](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.12...@standardnotes/home-server@1.15.13) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.12](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.11...@standardnotes/home-server@1.15.12) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.11](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.10...@standardnotes/home-server@1.15.11) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.10](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.9...@standardnotes/home-server@1.15.10) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.9](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.8...@standardnotes/home-server@1.15.9) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.8](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.7...@standardnotes/home-server@1.15.8) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.7](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.6...@standardnotes/home-server@1.15.7) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.15.7",
|
||||
"version": "1.15.18",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -20,6 +20,8 @@ DB_TYPE=mysql
|
||||
REDIS_URL=redis://cache
|
||||
CACHE_TYPE=redis
|
||||
|
||||
SNS_TOPIC_ARN=
|
||||
SNS_AWS_REGION=
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
S3_AWS_REGION=
|
||||
|
||||
@@ -3,6 +3,66 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.30.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.7...@standardnotes/revisions-server@1.30.8) (2023-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** add transition start info ([a1ee491](https://github.com/standardnotes/server/commit/a1ee491dc5835bfe9521b34f449085d2f13d5c68))
|
||||
* **revisions:** info logs on total revisions transitioned count ([e5c118c](https://github.com/standardnotes/server/commit/e5c118c262535971b42177db2a5a70d959b1c5d7))
|
||||
|
||||
## [1.30.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.6...@standardnotes/revisions-server@1.30.7) (2023-08-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** add more verbose messages about failures in revision transitioning ([a4929af](https://github.com/standardnotes/server/commit/a4929af2ee3df4db9f57c2b0fb250b6828095421))
|
||||
* **revisions:** revisions transition check for total count at the end ([095811d](https://github.com/standardnotes/server/commit/095811dda929e6947af36e096a659f18f1b5f8b8))
|
||||
|
||||
## [1.30.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.5...@standardnotes/revisions-server@1.30.6) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.30.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.4...@standardnotes/revisions-server@1.30.5) (2023-08-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** indiciate if revision did not save ([a6039bd](https://github.com/standardnotes/server/commit/a6039bd99ac37cc5ee4487336fa42412722c7815))
|
||||
|
||||
## [1.30.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.3...@standardnotes/revisions-server@1.30.4) (2023-08-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* transitionining revisions ([#801](https://github.com/standardnotes/server/issues/801)) ([596a0f1](https://github.com/standardnotes/server/commit/596a0f1a0221ab0636c4c04d17a28c57fe74b620))
|
||||
|
||||
## [1.30.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.2...@standardnotes/revisions-server@1.30.3) (2023-08-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* mongo delete queries ([ec35f46](https://github.com/standardnotes/server/commit/ec35f46d457ec5a5125dc1d0f1a14fb262012caa))
|
||||
|
||||
## [1.30.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.1...@standardnotes/revisions-server@1.30.2) (2023-08-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** mongo queries ([6ce42a0](https://github.com/standardnotes/server/commit/6ce42a0101169dd316624651b47a34f87ca35299))
|
||||
|
||||
## [1.30.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.30.0...@standardnotes/revisions-server@1.30.1) (2023-08-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** message in logs for trasitions status updated ([d722206](https://github.com/standardnotes/server/commit/d722206916d358e757f1cad7efeaf9881a1c6246))
|
||||
|
||||
# [1.30.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.29.0...@standardnotes/revisions-server@1.30.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
* add a way to trigger transition procedure for revisions ([#798](https://github.com/standardnotes/server/issues/798)) ([25ffd6b](https://github.com/standardnotes/server/commit/25ffd6b8036117b33584c6d948bb0867b637ae65))
|
||||
|
||||
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.28.0...@standardnotes/revisions-server@1.29.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add procedure for transitioning data from primary to secondary database ([#787](https://github.com/standardnotes/server/issues/787)) ([fe273a9](https://github.com/standardnotes/server/commit/fe273a9107585b5953c8de1d0f8afb951f891bca))
|
||||
|
||||
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.27.0...@standardnotes/revisions-server@1.28.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.28.0",
|
||||
"version": "1.30.8",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -26,6 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.332.0",
|
||||
"@aws-sdk/client-sns": "^3.332.0",
|
||||
"@aws-sdk/client-sqs": "^3.332.0",
|
||||
"@standardnotes/api": "^1.26.26",
|
||||
"@standardnotes/common": "workspace:^",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ControllerContainer, ControllerContainerInterface, MapperInterface } fr
|
||||
import { Container, interfaces } from 'inversify'
|
||||
import { MongoRepository, Repository } from 'typeorm'
|
||||
import * as winston from 'winston'
|
||||
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
|
||||
|
||||
import { Revision } from '../Domain/Revision/Revision'
|
||||
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import {
|
||||
SQSNewRelicEventMessageHandler,
|
||||
@@ -32,6 +34,7 @@ import {
|
||||
SQSDomainEventSubscriberFactory,
|
||||
DirectCallEventMessageHandler,
|
||||
DirectCallDomainEventPublisher,
|
||||
SNSDomainEventPublisher,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
@@ -54,6 +57,11 @@ import { RevisionRepositoryResolverInterface } from '../Domain/Revision/Revision
|
||||
import { TypeORMRevisionRepositoryResolver } from '../Infra/TypeORM/TypeORMRevisionRepositoryResolver'
|
||||
import { RevisionMetadataHttpRepresentation } from '../Mapping/Http/RevisionMetadataHttpRepresentation'
|
||||
import { RevisionHttpRepresentation } from '../Mapping/Http/RevisionHttpRepresentation'
|
||||
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
|
||||
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
@@ -97,6 +105,8 @@ export class ContainerConfigLoader {
|
||||
}
|
||||
container.bind<winston.Logger>(TYPES.Revisions_Logger).toConstantValue(logger)
|
||||
|
||||
container.bind<TimerInterface>(TYPES.Revisions_Timer).toDynamicValue(() => new Timer())
|
||||
|
||||
const appDataSource = new AppDataSource(env)
|
||||
await appDataSource.initialize()
|
||||
|
||||
@@ -107,6 +117,85 @@ export class ContainerConfigLoader {
|
||||
container.bind(TYPES.Revisions_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||
container.bind(TYPES.Revisions_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
|
||||
|
||||
if (!isConfiguredForHomeServer) {
|
||||
// env vars
|
||||
container.bind(TYPES.Revisions_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
|
||||
container.bind(TYPES.Revisions_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.Revisions_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
|
||||
container.bind(TYPES.Revisions_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
|
||||
container.bind(TYPES.Revisions_S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true))
|
||||
|
||||
container.bind<SNSClient>(TYPES.Revisions_SNS).toDynamicValue((context: interfaces.Context) => {
|
||||
const env: Env = context.container.get(TYPES.Revisions_Env)
|
||||
|
||||
const snsConfig: SNSClientConfig = {
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SNS_AWS_REGION', true),
|
||||
}
|
||||
if (env.get('SNS_ENDPOINT', true)) {
|
||||
snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
|
||||
}
|
||||
if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
|
||||
snsConfig.credentials = {
|
||||
accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
|
||||
return new SNSClient(snsConfig)
|
||||
})
|
||||
|
||||
container
|
||||
.bind<DomainEventPublisherInterface>(TYPES.Revisions_DomainEventPublisher)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new SNSDomainEventPublisher(
|
||||
context.container.get(TYPES.Revisions_SNS),
|
||||
context.container.get(TYPES.Revisions_SNS_TOPIC_ARN),
|
||||
)
|
||||
})
|
||||
|
||||
container.bind<SQSClient>(TYPES.Revisions_SQS).toDynamicValue((context: interfaces.Context) => {
|
||||
const env: Env = context.container.get(TYPES.Revisions_Env)
|
||||
|
||||
const sqsConfig: SQSClientConfig = {
|
||||
region: env.get('SQS_AWS_REGION'),
|
||||
}
|
||||
if (env.get('SQS_ENDPOINT', true)) {
|
||||
sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
|
||||
}
|
||||
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
|
||||
sqsConfig.credentials = {
|
||||
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
|
||||
return new SQSClient(sqsConfig)
|
||||
})
|
||||
|
||||
container.bind<S3Client | undefined>(TYPES.Revisions_S3).toDynamicValue((context: interfaces.Context) => {
|
||||
const env: Env = context.container.get(TYPES.Revisions_Env)
|
||||
|
||||
let s3Client = undefined
|
||||
if (env.get('S3_AWS_REGION', true)) {
|
||||
s3Client = new S3Client({
|
||||
apiVersion: 'latest',
|
||||
region: env.get('S3_AWS_REGION', true),
|
||||
})
|
||||
}
|
||||
|
||||
return s3Client
|
||||
})
|
||||
} else {
|
||||
container
|
||||
.bind<DomainEventPublisherInterface>(TYPES.Revisions_DomainEventPublisher)
|
||||
.toConstantValue(directCallDomainEventPublisher)
|
||||
}
|
||||
|
||||
container
|
||||
.bind<DomainEventFactoryInterface>(TYPES.Revisions_DomainEventFactory)
|
||||
.toConstantValue(new DomainEventFactory(container.get(TYPES.Revisions_Timer)))
|
||||
|
||||
// Map
|
||||
container
|
||||
.bind<MapperInterface<RevisionMetadata, SQLRevision>>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper)
|
||||
@@ -172,8 +261,6 @@ export class ContainerConfigLoader {
|
||||
),
|
||||
)
|
||||
|
||||
container.bind<TimerInterface>(TYPES.Revisions_Timer).toDynamicValue(() => new Timer())
|
||||
|
||||
container
|
||||
.bind<GetRequiredRoleToViewRevision>(TYPES.Revisions_GetRequiredRoleToViewRevision)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
@@ -219,6 +306,30 @@ export class ContainerConfigLoader {
|
||||
container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser>(
|
||||
TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
|
||||
)
|
||||
.toConstantValue(
|
||||
new TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser(
|
||||
container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository),
|
||||
isSecondaryDatabaseEnabled
|
||||
? container.get<RevisionRepositoryInterface>(TYPES.Revisions_MongoDBRevisionRepository)
|
||||
: null,
|
||||
container.get<TimerInterface>(TYPES.Revisions_Timer),
|
||||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
|
||||
TYPES.Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
|
||||
)
|
||||
.toConstantValue(
|
||||
new TriggerTransitionFromPrimaryToSecondaryDatabaseForUser(
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Revisions_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Revisions_DomainEventFactory),
|
||||
),
|
||||
)
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||
@@ -248,46 +359,6 @@ export class ContainerConfigLoader {
|
||||
.bind<MapperInterface<Revision, string>>(TYPES.Revisions_RevisionItemStringMapper)
|
||||
.toDynamicValue(() => new RevisionItemStringMapper())
|
||||
|
||||
if (!isConfiguredForHomeServer) {
|
||||
// env vars
|
||||
container.bind(TYPES.Revisions_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
|
||||
container.bind(TYPES.Revisions_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
|
||||
container.bind(TYPES.Revisions_S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true))
|
||||
|
||||
container.bind<SQSClient>(TYPES.Revisions_SQS).toDynamicValue((context: interfaces.Context) => {
|
||||
const env: Env = context.container.get(TYPES.Revisions_Env)
|
||||
|
||||
const sqsConfig: SQSClientConfig = {
|
||||
region: env.get('SQS_AWS_REGION'),
|
||||
}
|
||||
if (env.get('SQS_ENDPOINT', true)) {
|
||||
sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
|
||||
}
|
||||
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
|
||||
sqsConfig.credentials = {
|
||||
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
|
||||
return new SQSClient(sqsConfig)
|
||||
})
|
||||
|
||||
container.bind<S3Client | undefined>(TYPES.Revisions_S3).toDynamicValue((context: interfaces.Context) => {
|
||||
const env: Env = context.container.get(TYPES.Revisions_Env)
|
||||
|
||||
let s3Client = undefined
|
||||
if (env.get('S3_AWS_REGION', true)) {
|
||||
s3Client = new S3Client({
|
||||
apiVersion: 'latest',
|
||||
region: env.get('S3_AWS_REGION', true),
|
||||
})
|
||||
}
|
||||
|
||||
return s3Client
|
||||
})
|
||||
}
|
||||
|
||||
container
|
||||
.bind<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository)
|
||||
.toConstantValue(
|
||||
@@ -326,11 +397,24 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Revisions_TransitionStatusUpdatedEventHandler)
|
||||
.toConstantValue(
|
||||
new TransitionStatusUpdatedEventHandler(
|
||||
container.get<TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser>(
|
||||
TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
|
||||
),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Revisions_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Revisions_DomainEventFactory),
|
||||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Revisions_AccountDeletionRequestedEventHandler)],
|
||||
['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
|
||||
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Revisions_TransitionStatusUpdatedEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -373,6 +457,9 @@ export class ContainerConfigLoader {
|
||||
container.get<DeleteRevision>(TYPES.Revisions_DeleteRevision),
|
||||
container.get<RevisionHttpMapper>(TYPES.Revisions_RevisionHttpMapper),
|
||||
container.get<RevisionMetadataHttpMapper>(TYPES.Revisions_RevisionMetadataHttpMapper),
|
||||
container.get<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
|
||||
TYPES.Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
|
||||
),
|
||||
container.get<ControllerContainerInterface>(TYPES.Revisions_ControllerContainer),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ const TYPES = {
|
||||
Revisions_DBConnection: Symbol.for('Revisions_DBConnection'),
|
||||
Revisions_Logger: Symbol.for('Revisions_Logger'),
|
||||
Revisions_SQS: Symbol.for('Revisions_SQS'),
|
||||
Revisions_SNS: Symbol.for('Revisions_SNS'),
|
||||
Revisions_S3: Symbol.for('Revisions_S3'),
|
||||
Revisions_Env: Symbol.for('Revisions_Env'),
|
||||
// Map
|
||||
@@ -27,6 +28,8 @@ const TYPES = {
|
||||
Revisions_SQS_AWS_REGION: Symbol.for('Revisions_SQS_AWS_REGION'),
|
||||
Revisions_S3_AWS_REGION: Symbol.for('Revisions_S3_AWS_REGION'),
|
||||
Revisions_S3_BACKUP_BUCKET_NAME: Symbol.for('Revisions_S3_BACKUP_BUCKET_NAME'),
|
||||
Revisions_SNS_TOPIC_ARN: Symbol.for('Revisions_SNS_TOPIC_ARN'),
|
||||
Revisions_SNS_AWS_REGION: Symbol.for('Revisions_SNS_AWS_REGION'),
|
||||
Revisions_NEW_RELIC_ENABLED: Symbol.for('Revisions_NEW_RELIC_ENABLED'),
|
||||
Revisions_VERSION: Symbol.for('Revisions_VERSION'),
|
||||
// use cases
|
||||
@@ -35,6 +38,12 @@ const TYPES = {
|
||||
Revisions_DeleteRevision: Symbol.for('Revisions_DeleteRevision'),
|
||||
Revisions_CopyRevisions: Symbol.for('Revisions_CopyRevisions'),
|
||||
Revisions_GetRequiredRoleToViewRevision: Symbol.for('Revisions_GetRequiredRoleToViewRevision'),
|
||||
Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
|
||||
'Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser',
|
||||
),
|
||||
Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
|
||||
'Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser',
|
||||
),
|
||||
// Controller
|
||||
Revisions_ControllerContainer: Symbol.for('Revisions_ControllerContainer'),
|
||||
Revisions_RevisionsController: Symbol.for('Revisions_RevisionsController'),
|
||||
@@ -43,10 +52,13 @@ const TYPES = {
|
||||
Revisions_ItemDumpedEventHandler: Symbol.for('Revisions_ItemDumpedEventHandler'),
|
||||
Revisions_AccountDeletionRequestedEventHandler: Symbol.for('Revisions_AccountDeletionRequestedEventHandler'),
|
||||
Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
|
||||
Revisions_TransitionStatusUpdatedEventHandler: Symbol.for('Revisions_TransitionStatusUpdatedEventHandler'),
|
||||
// Services
|
||||
Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
|
||||
Revisions_DomainEventSubscriberFactory: Symbol.for('Revisions_DomainEventSubscriberFactory'),
|
||||
Revisions_DomainEventMessageHandler: Symbol.for('Revisions_DomainEventMessageHandler'),
|
||||
Revisions_DomainEventPublisher: Symbol.for('Revisions_DomainEventPublisher'),
|
||||
Revisions_DomainEventFactory: Symbol.for('Revisions_DomainEventFactory'),
|
||||
Revisions_Timer: Symbol.for('Revisions_Timer'),
|
||||
// Inversify Express Controllers
|
||||
Revisions_BaseRevisionsController: Symbol.for('Revisions_BaseRevisionsController'),
|
||||
|
||||
27
packages/revisions/src/Domain/Event/DomainEventFactory.ts
Normal file
27
packages/revisions/src/Domain/Event/DomainEventFactory.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* istanbul ignore file */
|
||||
import { DomainEventService, TransitionStatusUpdatedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
createTransitionStatusUpdatedEvent(dto: {
|
||||
userUuid: string
|
||||
transitionType: 'items' | 'revisions'
|
||||
status: 'STARTED' | 'FAILED' | 'FINISHED'
|
||||
}): TransitionStatusUpdatedEvent {
|
||||
return {
|
||||
type: 'TRANSITION_STATUS_UPDATED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { TransitionStatusUpdatedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createTransitionStatusUpdatedEvent(dto: {
|
||||
userUuid: string
|
||||
transitionType: 'items' | 'revisions'
|
||||
status: 'STARTED' | 'FAILED' | 'FINISHED'
|
||||
}): TransitionStatusUpdatedEvent
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
TransitionStatusUpdatedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from '../UseCase/Transition/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser'
|
||||
|
||||
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private transitionRevisionsFromPrimaryToSecondaryDatabaseForUser: TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
|
||||
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'items') {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createTransitionStatusUpdatedEvent({
|
||||
userUuid: event.payload.userUuid,
|
||||
status: 'STARTED',
|
||||
transitionType: 'revisions',
|
||||
}),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'revisions') {
|
||||
const result = await this.transitionRevisionsFromPrimaryToSecondaryDatabaseForUser.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to transition revisions for user ${event.payload.userUuid}: ${result.getError()}`)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createTransitionStatusUpdatedEvent({
|
||||
userUuid: event.payload.userUuid,
|
||||
status: 'FAILED',
|
||||
transitionType: 'revisions',
|
||||
}),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createTransitionStatusUpdatedEvent({
|
||||
userUuid: event.payload.userUuid,
|
||||
status: 'FINISHED',
|
||||
transitionType: 'revisions',
|
||||
}),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContentType, Dates, Uuid } from '@standardnotes/domain-core'
|
||||
import { ContentType, Dates, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Revision } from './Revision'
|
||||
|
||||
@@ -19,4 +19,106 @@ describe('Revision', () => {
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().id).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should tell if a revision is identical to another revision', () => {
|
||||
const entity1 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
const entity2 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
expect(entity1.isIdenticalTo(entity2)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell if a revision is not identical to another revision', () => {
|
||||
const entity1 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
const entity2 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test2',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
expect(entity1.isIdenticalTo(entity2)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell if a revision is not identical to another revision id ids do not match', () => {
|
||||
const entity1 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
const entity2 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
expect(entity1.isIdenticalTo(entity2)).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,4 +10,18 @@ export class Revision extends Entity<RevisionProps> {
|
||||
static create(props: RevisionProps, id?: UniqueEntityId): Result<Revision> {
|
||||
return Result.ok<Revision>(new Revision(props, id))
|
||||
}
|
||||
|
||||
isIdenticalTo(revision: Revision): boolean {
|
||||
if (this._id.toString() !== revision._id.toString()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const stringifiedThis = JSON.stringify(this.props)
|
||||
const stringifiedRevision = JSON.stringify(revision.props)
|
||||
|
||||
const base64This = Buffer.from(stringifiedThis).toString('base64')
|
||||
const base64Item = Buffer.from(stringifiedRevision).toString('base64')
|
||||
|
||||
return base64This === base64Item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import { Revision } from './Revision'
|
||||
import { RevisionMetadata } from './RevisionMetadata'
|
||||
|
||||
export interface RevisionRepositoryInterface {
|
||||
countByUserUuid(userUuid: Uuid): Promise<number>
|
||||
removeByUserUuid(userUuid: Uuid): Promise<void>
|
||||
removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void>
|
||||
findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null>
|
||||
findByItemUuid(itemUuid: Uuid): Promise<Array<Revision>>
|
||||
findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>>
|
||||
updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void>
|
||||
save(revision: Revision): Promise<Revision>
|
||||
findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Array<Revision>>
|
||||
save(revision: Revision): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { RevisionRepositoryInterface } from '../../../Revision/RevisionRepositoryInterface'
|
||||
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from './TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser'
|
||||
import { Revision } from '../../../Revision/Revision'
|
||||
import { ContentType, Dates, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
|
||||
let primaryRevisionRepository: RevisionRepositoryInterface
|
||||
let secondaryRevisionRepository: RevisionRepositoryInterface | null
|
||||
let logger: Logger
|
||||
let primaryRevision1: Revision
|
||||
let primaryRevision2: Revision
|
||||
let secondaryRevision1: Revision
|
||||
let secondaryRevision2: Revision
|
||||
let timer: TimerInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser(
|
||||
primaryRevisionRepository,
|
||||
secondaryRevisionRepository,
|
||||
timer,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
primaryRevision1 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
primaryRevision2 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc2d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
secondaryRevision1 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
secondaryRevision2 = Revision.create(
|
||||
{
|
||||
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc2d').getValue(),
|
||||
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
|
||||
content: 'test',
|
||||
contentType: ContentType.create('Note').getValue(),
|
||||
itemsKeyId: 'test',
|
||||
encItemKey: 'test',
|
||||
authHash: 'test',
|
||||
creationDate: new Date(1),
|
||||
dates: Dates.create(new Date(1), new Date(2)).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
primaryRevisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
primaryRevisionRepository.countByUserUuid = jest.fn().mockResolvedValue(2)
|
||||
primaryRevisionRepository.findByUserUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce([primaryRevision1])
|
||||
.mockResolvedValueOnce([primaryRevision2])
|
||||
.mockResolvedValueOnce([primaryRevision1])
|
||||
.mockResolvedValueOnce([primaryRevision2])
|
||||
primaryRevisionRepository.removeByUserUuid = jest.fn().mockResolvedValue(undefined)
|
||||
|
||||
secondaryRevisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
secondaryRevisionRepository.save = jest.fn().mockResolvedValue(true)
|
||||
secondaryRevisionRepository.removeByUserUuid = jest.fn().mockResolvedValue(undefined)
|
||||
secondaryRevisionRepository.countByUserUuid = jest.fn().mockResolvedValue(2)
|
||||
secondaryRevisionRepository.findOneByUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(secondaryRevision1)
|
||||
.mockResolvedValueOnce(secondaryRevision2)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
logger.info = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.sleep = jest.fn()
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
timer.convertMicrosecondsToTimeStructure = jest.fn().mockReturnValue({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
milliseconds: 0,
|
||||
})
|
||||
})
|
||||
|
||||
describe('successfull transition', () => {
|
||||
it('should transition Revisions from primary to secondary database', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(2)
|
||||
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
)
|
||||
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenCalledTimes(4)
|
||||
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(1, {
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
})
|
||||
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(2, {
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
limit: 1,
|
||||
offset: 1,
|
||||
})
|
||||
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(3, {
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
})
|
||||
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(4, {
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
limit: 1,
|
||||
offset: 1,
|
||||
})
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).save).toHaveBeenCalledTimes(2)
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).save).toHaveBeenCalledWith(primaryRevision1)
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).save).toHaveBeenCalledWith(primaryRevision2)
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).not.toHaveBeenCalled()
|
||||
expect(primaryRevisionRepository.removeByUserUuid).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should log an error if deleting Revisions from primary database fails', async () => {
|
||||
primaryRevisionRepository.removeByUserUuid = jest.fn().mockRejectedValue(new Error('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 clean up primary database revisions for user 00000000-0000-0000-0000-000000000000: Errored when deleting revisions for user 00000000-0000-0000-0000-000000000000: error',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('failed transition', () => {
|
||||
it('should remove Revisions from secondary database if integrity check fails', async () => {
|
||||
const secondaryRevision2WithDifferentContent = Revision.create({
|
||||
...secondaryRevision2.props,
|
||||
content: 'different-content',
|
||||
}).getValue()
|
||||
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).findOneByUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(secondaryRevision1)
|
||||
.mockResolvedValueOnce(secondaryRevision2WithDifferentContent)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual(
|
||||
'Revision 00000000-0000-0000-0000-000000000001 is not identical in primary and secondary database',
|
||||
)
|
||||
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove Revisions from secondary database if migrating Revisions fails', async () => {
|
||||
primaryRevisionRepository.findByUserUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce([primaryRevision1])
|
||||
.mockRejectedValueOnce(new Error('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual(
|
||||
'Errored when migrating revisions for user 00000000-0000-0000-0000-000000000000: error',
|
||||
)
|
||||
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return an error for a specific revision if it errors when saving to secondary database', async () => {
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).save = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(true)
|
||||
.mockRejectedValueOnce(new Error('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual(
|
||||
'Errored when saving revision 00000000-0000-0000-0000-000000000001 to secondary database: error',
|
||||
)
|
||||
})
|
||||
|
||||
it('should log an error if deleting Revisions from secondary database fails upon migration failure', async () => {
|
||||
primaryRevisionRepository.findByUserUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce([primaryRevision1])
|
||||
.mockRejectedValueOnce(new Error('error'))
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Failed to clean up secondary database revisions for user 00000000-0000-0000-0000-000000000000: Errored when deleting revisions for user 00000000-0000-0000-0000-000000000000: error',
|
||||
)
|
||||
})
|
||||
|
||||
it('should log an error if deleting Revisions from secondary database fails upon integrity check failure', async () => {
|
||||
const secondaryRevision2WithDifferentContent = Revision.create({
|
||||
...secondaryRevision2.props,
|
||||
content: 'different-content',
|
||||
}).getValue()
|
||||
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).findOneByUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(secondaryRevision1)
|
||||
.mockResolvedValueOnce(secondaryRevision2WithDifferentContent)
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Failed to clean up secondary database revisions for user 00000000-0000-0000-0000-000000000000: Errored when deleting revisions for user 00000000-0000-0000-0000-000000000000: error',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not perform the transition if secondary Revision repository is not set', async () => {
|
||||
secondaryRevisionRepository = null
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Secondary revision repository is not set')
|
||||
|
||||
expect(primaryRevisionRepository.countByUserUuid).not.toHaveBeenCalled()
|
||||
expect(primaryRevisionRepository.findByUserUuid).not.toHaveBeenCalled()
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not perform the transition if the user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid-uuid')
|
||||
|
||||
expect(primaryRevisionRepository.countByUserUuid).not.toHaveBeenCalled()
|
||||
expect(primaryRevisionRepository.findByUserUuid).not.toHaveBeenCalled()
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail integrity check if the Revision count is not the same in both databases', async () => {
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid = jest.fn().mockResolvedValue(1)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual(
|
||||
'Total revisions count for user 00000000-0000-0000-0000-000000000000 in primary database (2) does not match total revisions count in secondary database (1)',
|
||||
)
|
||||
|
||||
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(2)
|
||||
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
)
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid).toHaveBeenCalledTimes(1)
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should fail if one Revision is not found in the secondary database', async () => {
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).findOneByUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(secondaryRevision1)
|
||||
.mockResolvedValueOnce(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Revision 00000000-0000-0000-0000-000000000001 not found in secondary database')
|
||||
|
||||
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(2)
|
||||
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
|
||||
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
)
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid).not.toHaveBeenCalled()
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should fail if an error is thrown during integrity check between primary and secondary database', async () => {
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Errored when checking integrity between primary and secondary database: error')
|
||||
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should fail if a revisions did not save in the secondary database', async () => {
|
||||
;(secondaryRevisionRepository as RevisionRepositoryInterface).save = jest.fn().mockResolvedValue(false)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual(
|
||||
'Failed to save revision 00000000-0000-0000-0000-000000000000 to secondary database',
|
||||
)
|
||||
|
||||
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
|
||||
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO } from './TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO'
|
||||
import { RevisionRepositoryInterface } from '../../../Revision/RevisionRepositoryInterface'
|
||||
|
||||
export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private primaryRevisionsRepository: RevisionRepositoryInterface,
|
||||
private secondRevisionsRepository: RevisionRepositoryInterface | null,
|
||||
private timer: TimerInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO): Promise<Result<void>> {
|
||||
if (this.secondRevisionsRepository === null) {
|
||||
return Result.fail('Secondary revision repository is not set')
|
||||
}
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const migrationTimeStart = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
this.logger.info(`Transitioning revisions for user ${userUuid.value}`)
|
||||
|
||||
const migrationResult = await this.migrateRevisionsForUser(userUuid)
|
||||
if (migrationResult.isFailed()) {
|
||||
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
|
||||
if (cleanupResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
return Result.fail(migrationResult.getError())
|
||||
}
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
|
||||
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid)
|
||||
if (integrityCheckResult.isFailed()) {
|
||||
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
|
||||
if (cleanupResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
return Result.fail(integrityCheckResult.getError())
|
||||
}
|
||||
|
||||
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.primaryRevisionsRepository)
|
||||
if (cleanupResult.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to clean up primary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const migrationTimeEnd = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
const migrationDuration = migrationTimeEnd - migrationTimeStart
|
||||
const migrationDurationTimeStructure = this.timer.convertMicrosecondsToTimeStructure(migrationDuration)
|
||||
|
||||
this.logger.info(
|
||||
`Transitioned revisions for user ${userUuid.value} in ${migrationDurationTimeStructure.hours}h ${migrationDurationTimeStructure.minutes}m ${migrationDurationTimeStructure.seconds}s ${migrationDurationTimeStructure.milliseconds}ms`,
|
||||
)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async migrateRevisionsForUser(userUuid: Uuid): Promise<Result<void>> {
|
||||
try {
|
||||
const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
let totalRevisionsCountTransitionedToSecondary = 0
|
||||
const pageSize = 1
|
||||
const totalPages = Math.ceil(totalRevisionsCountForUser / pageSize)
|
||||
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
|
||||
const query = {
|
||||
userUuid: userUuid,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
}
|
||||
|
||||
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
|
||||
|
||||
for (const revision of revisions) {
|
||||
try {
|
||||
const didSave = await (this.secondRevisionsRepository as RevisionRepositoryInterface).save(revision)
|
||||
if (!didSave) {
|
||||
return Result.fail(`Failed to save revision ${revision.id.toString()} to secondary database`)
|
||||
}
|
||||
totalRevisionsCountTransitionedToSecondary++
|
||||
} catch (error) {
|
||||
return Result.fail(
|
||||
`Errored when saving revision ${revision.id.toString()} to secondary database: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Transitioned ${totalRevisionsCountTransitionedToSecondary} revisions to secondary database`)
|
||||
|
||||
return Result.ok()
|
||||
} catch (error) {
|
||||
return Result.fail(`Errored when migrating revisions for user ${userUuid.value}: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteRevisionsForUser(
|
||||
userUuid: Uuid,
|
||||
revisionRepository: RevisionRepositoryInterface,
|
||||
): Promise<Result<void>> {
|
||||
try {
|
||||
await revisionRepository.removeByUserUuid(userUuid)
|
||||
|
||||
return Result.ok()
|
||||
} catch (error) {
|
||||
return Result.fail(`Errored when deleting revisions for user ${userUuid.value}: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async allowForSecondaryDatabaseToCatchUp(): Promise<void> {
|
||||
const twoSecondsInMilliseconds = 2_000
|
||||
await this.timer.sleep(twoSecondsInMilliseconds)
|
||||
}
|
||||
|
||||
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid: Uuid): Promise<Result<boolean>> {
|
||||
try {
|
||||
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
|
||||
|
||||
const pageSize = 1
|
||||
const totalPages = Math.ceil(totalRevisionsCountForUserInPrimary / pageSize)
|
||||
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
|
||||
const query = {
|
||||
userUuid: userUuid,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
}
|
||||
|
||||
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
|
||||
|
||||
for (const revision of revisions) {
|
||||
const revisionUuidOrError = Uuid.create(revision.id.toString())
|
||||
/* istanbul ignore if */
|
||||
if (revisionUuidOrError.isFailed()) {
|
||||
return Result.fail(revisionUuidOrError.getError())
|
||||
}
|
||||
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`)
|
||||
}
|
||||
|
||||
if (!revision.isIdenticalTo(revisionInSecondary)) {
|
||||
return Result.fail(`Revision ${revision.id.toString()} is not identical in primary and secondary database`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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})`,
|
||||
)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
} catch (error) {
|
||||
return Result.fail(
|
||||
`Errored when checking integrity between primary and secondary database: ${(error as Error).message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from './TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('TriggerTransitionFromPrimaryToSecondaryDatabaseForUser', () => {
|
||||
let domainEventPubliser: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new TriggerTransitionFromPrimaryToSecondaryDatabaseForUser(domainEventPubliser, domainEventFactory)
|
||||
|
||||
beforeEach(() => {
|
||||
domainEventPubliser = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPubliser.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createTransitionStatusUpdatedEvent = jest.fn()
|
||||
})
|
||||
|
||||
it('should publish transition status updated event', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(domainEventPubliser.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO } from './TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class TriggerTransitionFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private domainEventPubliser: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO): Promise<Result<void>> {
|
||||
const event = this.domainEventFactory.createTransitionStatusUpdatedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
status: 'STARTED',
|
||||
transitionType: 'revisions',
|
||||
})
|
||||
|
||||
await this.domainEventPubliser.publish(event)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { controller, httpDelete, httpGet, results } from 'inversify-express-utils'
|
||||
import { controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils'
|
||||
import { inject } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
@@ -12,8 +12,9 @@ import { Revision } from '../../Domain/Revision/Revision'
|
||||
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
|
||||
import { RevisionHttpRepresentation } from '../../Mapping/Http/RevisionHttpRepresentation'
|
||||
import { RevisionMetadataHttpRepresentation } from '../../Mapping/Http/RevisionMetadataHttpRepresentation'
|
||||
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
|
||||
|
||||
@controller('/items/:itemUuid/revisions', TYPES.Revisions_ApiGatewayAuthMiddleware)
|
||||
@controller('', TYPES.Revisions_ApiGatewayAuthMiddleware)
|
||||
export class AnnotatedRevisionsController extends BaseRevisionsController {
|
||||
constructor(
|
||||
@inject(TYPES.Revisions_GetRevisionsMetada) override getRevisionsMetadata: GetRevisionsMetada,
|
||||
@@ -23,22 +24,36 @@ export class AnnotatedRevisionsController extends BaseRevisionsController {
|
||||
override revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>,
|
||||
@inject(TYPES.Revisions_RevisionMetadataHttpMapper)
|
||||
override revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>,
|
||||
@inject(TYPES.Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser)
|
||||
override triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
|
||||
) {
|
||||
super(getRevisionsMetadata, doGetRevision, doDeleteRevision, revisionHttpMapper, revisionMetadataHttpMapper)
|
||||
super(
|
||||
getRevisionsMetadata,
|
||||
doGetRevision,
|
||||
doDeleteRevision,
|
||||
revisionHttpMapper,
|
||||
revisionMetadataHttpMapper,
|
||||
triggerTransitionFromPrimaryToSecondaryDatabaseForUser,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
@httpGet('/items/:itemUuid/revisions')
|
||||
override async getRevisions(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.getRevisions(request, response)
|
||||
}
|
||||
|
||||
@httpGet('/:uuid')
|
||||
@httpGet('/items/:itemUuid/revisions/:uuid')
|
||||
override async getRevision(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.getRevision(request, response)
|
||||
}
|
||||
|
||||
@httpDelete('/:uuid')
|
||||
@httpDelete('/items/:itemUuid/revisions/:uuid')
|
||||
override async deleteRevision(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.deleteRevision(request, response)
|
||||
}
|
||||
|
||||
@httpPost('/revisions/transition')
|
||||
override async transition(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.transition(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { GetRevision } from '../../../Domain/UseCase/GetRevision/GetRevision'
|
||||
import { GetRevisionsMetada } from '../../../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
|
||||
import { RevisionHttpRepresentation } from '../../../Mapping/Http/RevisionHttpRepresentation'
|
||||
import { RevisionMetadataHttpRepresentation } from '../../../Mapping/Http/RevisionMetadataHttpRepresentation'
|
||||
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
|
||||
|
||||
export class BaseRevisionsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -19,6 +20,7 @@ export class BaseRevisionsController extends BaseHttpController {
|
||||
protected doDeleteRevision: DeleteRevision,
|
||||
protected revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>,
|
||||
protected revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>,
|
||||
protected triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
|
||||
private controllerContainer?: ControllerContainerInterface,
|
||||
) {
|
||||
super()
|
||||
@@ -27,6 +29,7 @@ export class BaseRevisionsController extends BaseHttpController {
|
||||
this.controllerContainer.register('revisions.revisions.getRevisions', this.getRevisions.bind(this))
|
||||
this.controllerContainer.register('revisions.revisions.getRevision', this.getRevision.bind(this))
|
||||
this.controllerContainer.register('revisions.revisions.deleteRevision', this.deleteRevision.bind(this))
|
||||
this.controllerContainer.register('revisions.revisions.transition', this.transition.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,4 +102,23 @@ export class BaseRevisionsController extends BaseHttpController {
|
||||
message: revisionOrError.getValue(),
|
||||
})
|
||||
}
|
||||
|
||||
async transition(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: { message: result.getError() },
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,36 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async countByUserUuid(userUuid: Uuid): Promise<number> {
|
||||
return this.mongoRepository.count({ userUuid: { $eq: userUuid.value } })
|
||||
}
|
||||
|
||||
async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Revision[]> {
|
||||
const mongoRevisions = await this.mongoRepository.find({
|
||||
where: { userUuid: { $eq: dto.userUuid.value } },
|
||||
order: {
|
||||
createdAt: 'ASC',
|
||||
},
|
||||
skip: dto.offset,
|
||||
take: dto.limit,
|
||||
})
|
||||
|
||||
const revisions = []
|
||||
for (const mongoRevision of mongoRevisions) {
|
||||
revisions.push(this.revisionMapper.toDomain(mongoRevision))
|
||||
}
|
||||
|
||||
return revisions
|
||||
}
|
||||
|
||||
async removeByUserUuid(userUuid: Uuid): Promise<void> {
|
||||
await this.mongoRepository.deleteMany({ where: { userUuid: { $eq: userUuid.value } } })
|
||||
await this.mongoRepository.deleteMany({ userUuid: userUuid.value })
|
||||
}
|
||||
|
||||
async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
|
||||
await this.mongoRepository.deleteOne({
|
||||
where: {
|
||||
$and: [
|
||||
{ _id: { $eq: BSON.UUID.createFromHexString(revisionUuid.value) } },
|
||||
{ userUuid: { $eq: userUuid.value } },
|
||||
],
|
||||
},
|
||||
_id: { $eq: BSON.UUID.createFromHexString(revisionUuid.value) },
|
||||
userUuid: { $eq: userUuid.value },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,12 +123,12 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
|
||||
)
|
||||
}
|
||||
|
||||
async save(revision: Revision): Promise<Revision> {
|
||||
async save(revision: Revision): Promise<boolean> {
|
||||
const persistence = this.revisionMapper.toProjection(revision)
|
||||
|
||||
const { _id, ...rest } = persistence
|
||||
|
||||
await this.mongoRepository.updateOne(
|
||||
const updateResult = await this.mongoRepository.updateOne(
|
||||
{ _id: { $eq: _id } },
|
||||
{
|
||||
$set: rest,
|
||||
@@ -118,6 +136,6 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
|
||||
{ upsert: true },
|
||||
)
|
||||
|
||||
return revision
|
||||
return updateResult.acknowledged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,37 @@ export class SQLRevisionRepository implements RevisionRepositoryInterface {
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async countByUserUuid(userUuid: Uuid): Promise<number> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
|
||||
.getCount()
|
||||
}
|
||||
|
||||
async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Revision[]> {
|
||||
const queryBuilder = this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.where('user_uuid = :userUuid', { userUuid: dto.userUuid.value })
|
||||
.orderBy('created_at', 'ASC')
|
||||
|
||||
if (dto.offset !== undefined) {
|
||||
queryBuilder.skip(dto.offset)
|
||||
}
|
||||
|
||||
if (dto.limit !== undefined) {
|
||||
queryBuilder.take(dto.limit)
|
||||
}
|
||||
|
||||
const sqlRevisions = await queryBuilder.getMany()
|
||||
|
||||
const revisions = []
|
||||
for (const sqlRevision of sqlRevisions) {
|
||||
revisions.push(this.revisionMapper.toDomain(sqlRevision))
|
||||
}
|
||||
|
||||
return revisions
|
||||
}
|
||||
|
||||
async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
@@ -75,12 +106,12 @@ export class SQLRevisionRepository implements RevisionRepositoryInterface {
|
||||
return this.revisionMapper.toDomain(SQLRevision)
|
||||
}
|
||||
|
||||
async save(revision: Revision): Promise<Revision> {
|
||||
async save(revision: Revision): Promise<boolean> {
|
||||
const SQLRevision = this.revisionMapper.toProjection(revision)
|
||||
|
||||
await this.ormRepository.save(SQLRevision)
|
||||
|
||||
return revision
|
||||
return true
|
||||
}
|
||||
|
||||
async findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>> {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.32](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.31...@standardnotes/scheduler-server@1.20.32) (2023-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove the alive and kicking info logs on workers ([1bef127](https://github.com/standardnotes/server/commit/1bef1279e6dbf3cbdfa87e44aa9108ed6dbb3b0f))
|
||||
|
||||
## [1.20.31](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.30...@standardnotes/scheduler-server@1.20.31) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.30](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.29...@standardnotes/scheduler-server@1.20.30) (2023-08-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.29](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.28...@standardnotes/scheduler-server@1.20.29) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.28](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.27...@standardnotes/scheduler-server@1.20.28) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -22,6 +22,4 @@ void container.load().then((container) => {
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.28",
|
||||
"version": "1.20.32",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.90.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.89.0...@standardnotes/syncing-server@1.90.0) (2023-09-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove the alive and kicking info logs on workers ([1bef127](https://github.com/standardnotes/syncing-server-js/commit/1bef1279e6dbf3cbdfa87e44aa9108ed6dbb3b0f))
|
||||
|
||||
### Features
|
||||
|
||||
* send websocket event to user when a message is sent ([#802](https://github.com/standardnotes/syncing-server-js/issues/802)) ([9a568b0](https://github.com/standardnotes/syncing-server-js/commit/9a568b0f73078ab74d4771bac469903a124e67da))
|
||||
|
||||
# [1.89.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.88.3...@standardnotes/syncing-server@1.89.0) (2023-08-31)
|
||||
|
||||
### Features
|
||||
|
||||
* add sending notifications to user via websockets ([#799](https://github.com/standardnotes/syncing-server-js/issues/799)) ([c0722b1](https://github.com/standardnotes/syncing-server-js/commit/c0722b173b71d696568d8e8c5095a22fd219bef6))
|
||||
|
||||
## [1.88.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.88.2...@standardnotes/syncing-server@1.88.3) (2023-08-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** persistence mapping for deleted field ([baf4b2c](https://github.com/standardnotes/syncing-server-js/commit/baf4b2c1d205929be8c330450dca16c18ad5cdd6))
|
||||
|
||||
## [1.88.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.88.1...@standardnotes/syncing-server@1.88.2) (2023-08-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* transitionining revisions ([#801](https://github.com/standardnotes/syncing-server-js/issues/801)) ([596a0f1](https://github.com/standardnotes/syncing-server-js/commit/596a0f1a0221ab0636c4c04d17a28c57fe74b620))
|
||||
|
||||
## [1.88.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.88.0...@standardnotes/syncing-server@1.88.1) (2023-08-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* mongo delete queries ([ec35f46](https://github.com/standardnotes/syncing-server-js/commit/ec35f46d457ec5a5125dc1d0f1a14fb262012caa))
|
||||
|
||||
# [1.88.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.87.0...@standardnotes/syncing-server@1.88.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add procedure for transitioning data from primary to secondary database ([#787](https://github.com/standardnotes/syncing-server-js/issues/787)) ([fe273a9](https://github.com/standardnotes/syncing-server-js/commit/fe273a9107585b5953c8de1d0f8afb951f891bca))
|
||||
|
||||
# [1.87.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.86.0...@standardnotes/syncing-server@1.87.0) (2023-08-30)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -20,6 +20,4 @@ void container.load().then((container) => {
|
||||
TYPES.Sync_DomainEventSubscriberFactory,
|
||||
)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.87.0",
|
||||
"version": "1.90.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -161,6 +161,7 @@ import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../Domai
|
||||
import { SQLItem } from '../Infra/TypeORM/SQLItem'
|
||||
import { SQLItemPersistenceMapper } from '../Mapping/Persistence/SQLItemPersistenceMapper'
|
||||
import { SQLItemRepository } from '../Infra/TypeORM/SQLItemRepository'
|
||||
import { SendEventToClient } from '../Domain/UseCase/Syncing/SendEventToClient/SendEventToClient'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -580,10 +581,24 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SendEventToClient>(TYPES.Sync_SendEventToClient)
|
||||
.toConstantValue(
|
||||
new SendEventToClient(
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser)
|
||||
.toConstantValue(
|
||||
new AddNotificationForUser(container.get(TYPES.Sync_NotificationRepository), container.get(TYPES.Sync_Timer)),
|
||||
new AddNotificationForUser(
|
||||
container.get<NotificationRepositoryInterface>(TYPES.Sync_NotificationRepository),
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers)
|
||||
@@ -768,7 +783,13 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SendMessageToUser>(TYPES.Sync_SendMessageToUser)
|
||||
.toConstantValue(
|
||||
new SendMessageToUser(container.get(TYPES.Sync_MessageRepository), container.get(TYPES.Sync_Timer)),
|
||||
new SendMessageToUser(
|
||||
container.get<MessageRepositoryInterface>(TYPES.Sync_MessageRepository),
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteMessage>(TYPES.Sync_DeleteMessage)
|
||||
|
||||
@@ -87,6 +87,7 @@ const TYPES = {
|
||||
Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
|
||||
'Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser',
|
||||
),
|
||||
Sync_SendEventToClient: Symbol.for('Sync_SendEventToClient'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
|
||||
@@ -5,8 +5,11 @@ import {
|
||||
EmailRequestedEvent,
|
||||
ItemDumpedEvent,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
MessageSentToUserEvent,
|
||||
NotificationAddedForUserEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
TransitionStatusUpdatedEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
@@ -14,21 +17,86 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
createTransitionStatusUpdatedEvent(userUuid: string, status: 'FINISHED' | 'FAILED'): TransitionStatusUpdatedEvent {
|
||||
createMessageSentToUserEvent(dto: {
|
||||
message: {
|
||||
uuid: string
|
||||
recipient_uuid: string
|
||||
sender_uuid: string
|
||||
encrypted_message: string
|
||||
replaceability_identifier: string | null
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
}): MessageSentToUserEvent {
|
||||
return {
|
||||
type: 'MESSAGE_SENT_TO_USER',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.message.recipient_uuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createNotificationAddedForUserEvent(dto: {
|
||||
notification: {
|
||||
uuid: string
|
||||
user_uuid: string
|
||||
type: string
|
||||
payload: string
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
}): NotificationAddedForUserEvent {
|
||||
return {
|
||||
type: 'NOTIFICATION_ADDED_FOR_USER',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.notification.user_uuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: string }): WebSocketMessageRequestedEvent {
|
||||
return {
|
||||
type: 'WEB_SOCKET_MESSAGE_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createTransitionStatusUpdatedEvent(dto: {
|
||||
userUuid: string
|
||||
transitionType: 'items' | 'revisions'
|
||||
status: 'STARTED' | 'FAILED' | 'FINISHED'
|
||||
}): TransitionStatusUpdatedEvent {
|
||||
return {
|
||||
type: 'TRANSITION_STATUS_UPDATED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: userUuid,
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: {
|
||||
userUuid,
|
||||
status,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,41 @@ import {
|
||||
EmailRequestedEvent,
|
||||
ItemDumpedEvent,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
MessageSentToUserEvent,
|
||||
NotificationAddedForUserEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
TransitionStatusUpdatedEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createTransitionStatusUpdatedEvent(
|
||||
userUuid: string,
|
||||
status: 'STARTED' | 'FAILED' | 'FINISHED',
|
||||
): TransitionStatusUpdatedEvent
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: string }): WebSocketMessageRequestedEvent
|
||||
createMessageSentToUserEvent(dto: {
|
||||
message: {
|
||||
uuid: string
|
||||
recipient_uuid: string
|
||||
sender_uuid: string
|
||||
encrypted_message: string
|
||||
replaceability_identifier: string | null
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
}): MessageSentToUserEvent
|
||||
createNotificationAddedForUserEvent(dto: {
|
||||
notification: {
|
||||
uuid: string
|
||||
user_uuid: string
|
||||
type: string
|
||||
payload: string
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
}): NotificationAddedForUserEvent
|
||||
createTransitionStatusUpdatedEvent(dto: {
|
||||
userUuid: string
|
||||
transitionType: 'items' | 'revisions'
|
||||
status: 'STARTED' | 'FAILED' | 'FINISHED'
|
||||
}): TransitionStatusUpdatedEvent
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: string
|
||||
|
||||
@@ -16,7 +16,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
|
||||
) {}
|
||||
|
||||
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
|
||||
if (event.payload.status === 'STARTED') {
|
||||
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'items') {
|
||||
const result = await this.transitionItemsFromPrimaryToSecondaryDatabaseForUser.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
@@ -25,14 +25,22 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
|
||||
this.logger.error(`Failed to transition items for user ${event.payload.userUuid}: ${result.getError()}`)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createTransitionStatusUpdatedEvent(event.payload.userUuid, 'FAILED'),
|
||||
this.domainEventFactory.createTransitionStatusUpdatedEvent({
|
||||
userUuid: event.payload.userUuid,
|
||||
status: 'FAILED',
|
||||
transitionType: 'items',
|
||||
}),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createTransitionStatusUpdatedEvent(event.payload.userUuid, 'FINISHED'),
|
||||
this.domainEventFactory.createTransitionStatusUpdatedEvent({
|
||||
userUuid: event.payload.userUuid,
|
||||
status: 'FINISHED',
|
||||
transitionType: 'items',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,21 @@ import { NotificationPayload, NotificationType, Result, Uuid } from '@standardno
|
||||
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
|
||||
import { Notification } from '../../../Notifications/Notification'
|
||||
import { AddNotificationForUser } from './AddNotificationForUser'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient'
|
||||
import { NotificationAddedForUserEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('AddNotificationForUser', () => {
|
||||
let notificationRepository: NotificationRepositoryInterface
|
||||
let timer: TimerInterface
|
||||
let payload: NotificationPayload
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let sendEventToClientUseCase: SendEventToClient
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new AddNotificationForUser(notificationRepository, timer)
|
||||
const createUseCase = () =>
|
||||
new AddNotificationForUser(notificationRepository, timer, domainEventFactory, sendEventToClientUseCase, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
notificationRepository = {} as jest.Mocked<NotificationRepositoryInterface>
|
||||
@@ -24,6 +32,17 @@ describe('AddNotificationForUser', () => {
|
||||
type: NotificationType.create(NotificationType.TYPES.RemovedFromSharedVault).getValue(),
|
||||
version: '1.0',
|
||||
}).getValue()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createNotificationAddedForUserEvent = jest.fn().mockReturnValue({
|
||||
type: 'NOTIFICATION_ADDED_FOR_USER',
|
||||
} as jest.Mocked<NotificationAddedForUserEvent>)
|
||||
|
||||
sendEventToClientUseCase = {} as jest.Mocked<SendEventToClient>
|
||||
sendEventToClientUseCase.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should save notification', async () => {
|
||||
@@ -84,4 +103,20 @@ describe('AddNotificationForUser', () => {
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('should log error if event could not be sent to client', async () => {
|
||||
sendEventToClientUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
|
||||
type: NotificationType.TYPES.RemovedFromSharedVault,
|
||||
payload,
|
||||
version: '1.0',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(logger.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,9 +4,18 @@ import { TimerInterface } from '@standardnotes/time'
|
||||
import { AddNotificationForUserDTO } from './AddNotificationForUserDTO'
|
||||
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
|
||||
import { Notification } from '../../../Notifications/Notification'
|
||||
import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class AddNotificationForUser implements UseCaseInterface<Notification> {
|
||||
constructor(private notificationRepository: NotificationRepositoryInterface, private timer: TimerInterface) {}
|
||||
constructor(
|
||||
private notificationRepository: NotificationRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private sendEventToClientUseCase: SendEventToClient,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: AddNotificationForUserDTO): Promise<Result<Notification>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
@@ -37,6 +46,27 @@ export class AddNotificationForUser implements UseCaseInterface<Notification> {
|
||||
|
||||
await this.notificationRepository.save(notification)
|
||||
|
||||
const event = this.domainEventFactory.createNotificationAddedForUserEvent({
|
||||
notification: {
|
||||
uuid: notification.id.toString(),
|
||||
user_uuid: notification.props.userUuid.value,
|
||||
type: notification.props.type.value,
|
||||
payload: notification.props.payload.toString(),
|
||||
created_at_timestamp: notification.props.timestamps.createdAt,
|
||||
updated_at_timestamp: notification.props.timestamps.updatedAt,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await this.sendEventToClientUseCase.execute({
|
||||
userUuid: userUuid.value,
|
||||
event,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to send notification added event to client for user ${userUuid.value}: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
return Result.ok(notification)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,21 @@ import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryIn
|
||||
import { SendMessageToUser } from './SendMessageToUser'
|
||||
import { Message } from '../../../Message/Message'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient'
|
||||
import { MessageSentToUserEvent } from '@standardnotes/domain-events'
|
||||
|
||||
describe('SendMessageToUser', () => {
|
||||
let messageRepository: MessageRepositoryInterface
|
||||
let timer: TimerInterface
|
||||
let existingMessage: Message
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let sendEventToClientUseCase: SendEventToClient
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new SendMessageToUser(messageRepository, timer)
|
||||
const createUseCase = () =>
|
||||
new SendMessageToUser(messageRepository, timer, domainEventFactory, sendEventToClientUseCase, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
existingMessage = {} as jest.Mocked<Message>
|
||||
@@ -21,6 +29,17 @@ describe('SendMessageToUser', () => {
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createMessageSentToUserEvent = jest.fn().mockReturnValue({
|
||||
type: 'MESSAGE_SENT_TO_USER',
|
||||
} as jest.Mocked<MessageSentToUserEvent>)
|
||||
|
||||
sendEventToClientUseCase = {} as jest.Mocked<SendEventToClient>
|
||||
sendEventToClientUseCase.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('saves a new message', async () => {
|
||||
@@ -104,4 +123,19 @@ describe('SendMessageToUser', () => {
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('should log error if event could not be sent to user', async () => {
|
||||
sendEventToClientUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
recipientUuid: '00000000-0000-0000-0000-000000000000',
|
||||
senderUuid: '00000000-0000-0000-0000-000000000000',
|
||||
encryptedMessage: 'encrypted-message',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(logger.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,9 +4,18 @@ import { TimerInterface } from '@standardnotes/time'
|
||||
import { SendMessageToUserDTO } from './SendMessageToUserDTO'
|
||||
import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface'
|
||||
import { Message } from '../../../Message/Message'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class SendMessageToUser implements UseCaseInterface<Message> {
|
||||
constructor(private messageRepository: MessageRepositoryInterface, private timer: TimerInterface) {}
|
||||
constructor(
|
||||
private messageRepository: MessageRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private sendEventToClientUseCase: SendEventToClient,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: SendMessageToUserDTO): Promise<Result<Message>> {
|
||||
const recipientUuidOrError = Uuid.create(dto.recipientUuid)
|
||||
@@ -54,6 +63,30 @@ export class SendMessageToUser implements UseCaseInterface<Message> {
|
||||
|
||||
await this.messageRepository.save(message)
|
||||
|
||||
const event = this.domainEventFactory.createMessageSentToUserEvent({
|
||||
message: {
|
||||
uuid: message.id.toString(),
|
||||
recipient_uuid: message.props.recipientUuid.value,
|
||||
sender_uuid: message.props.senderUuid.value,
|
||||
encrypted_message: message.props.encryptedMessage,
|
||||
replaceability_identifier: message.props.replaceabilityIdentifier,
|
||||
created_at_timestamp: message.props.timestamps.createdAt,
|
||||
updated_at_timestamp: message.props.timestamps.updatedAt,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await this.sendEventToClientUseCase.execute({
|
||||
userUuid: message.props.recipientUuid.value,
|
||||
event,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to send message sent event to client for user ${
|
||||
message.props.recipientUuid.value
|
||||
}: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
return Result.ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
DomainEventInterface,
|
||||
DomainEventPublisherInterface,
|
||||
WebSocketMessageRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SendEventToClient } from './SendEventToClient'
|
||||
|
||||
describe('SendEventToClient', () => {
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createUseCase = () => new SendEventToClient(domainEventFactory, domainEventPublisher)
|
||||
|
||||
beforeEach(() => {
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createWebSocketMessageRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<WebSocketMessageRequestedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should publish a WebSocketMessageRequestedEvent', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
event: {
|
||||
type: 'test',
|
||||
} as jest.Mocked<DomainEventInterface>,
|
||||
})
|
||||
|
||||
expect(domainEventFactory.createWebSocketMessageRequestedEvent).toHaveBeenCalledWith({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
message: JSON.stringify({
|
||||
type: 'test',
|
||||
}),
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledWith({} as jest.Mocked<WebSocketMessageRequestedEvent>)
|
||||
})
|
||||
|
||||
it('should return a failed result if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
event: {
|
||||
type: 'test',
|
||||
} as jest.Mocked<DomainEventInterface>,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { SendEventToClientDTO } from './SendEventToClientDTO'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class SendEventToClient implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: SendEventToClientDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const event = this.domainEventFactory.createWebSocketMessageRequestedEvent({
|
||||
userUuid: userUuid.value,
|
||||
message: JSON.stringify(dto.event),
|
||||
})
|
||||
|
||||
await this.domainEventPublisher.publish(event)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DomainEventInterface } from '@standardnotes/domain-events'
|
||||
|
||||
export interface SendEventToClientDTO {
|
||||
userUuid: string
|
||||
event: DomainEventInterface
|
||||
}
|
||||
@@ -117,6 +117,7 @@ describe('TransitionItemsFromPrimaryToSecondaryDatabaseForUser', () => {
|
||||
logger.info = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.sleep = jest.fn()
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
timer.convertMicrosecondsToTimeStructure = jest.fn().mockReturnValue({
|
||||
days: 0,
|
||||
|
||||
@@ -39,6 +39,8 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
return Result.fail(migrationResult.getError())
|
||||
}
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
|
||||
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid)
|
||||
if (integrityCheckResult.isFailed()) {
|
||||
const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
|
||||
@@ -70,6 +72,11 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async allowForSecondaryDatabaseToCatchUp(): Promise<void> {
|
||||
const twoSecondsInMilliseconds = 2_000
|
||||
await this.timer.sleep(twoSecondsInMilliseconds)
|
||||
}
|
||||
|
||||
private async migrateItemsForUser(userUuid: Uuid): Promise<Result<void>> {
|
||||
try {
|
||||
const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
|
||||
|
||||
@@ -11,7 +11,11 @@ export class TriggerTransitionFromPrimaryToSecondaryDatabaseForUser implements U
|
||||
) {}
|
||||
|
||||
async execute(dto: TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO): Promise<Result<void>> {
|
||||
const event = this.domainEventFactory.createTransitionStatusUpdatedEvent(dto.userUuid, 'STARTED')
|
||||
const event = this.domainEventFactory.createTransitionStatusUpdatedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
status: 'STARTED',
|
||||
transitionType: 'items',
|
||||
})
|
||||
|
||||
await this.domainEventPubliser.publish(event)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
|
||||
) {}
|
||||
|
||||
async deleteByUserUuid(userUuid: string): Promise<void> {
|
||||
await this.mongoRepository.deleteMany({ where: { userUuid } })
|
||||
await this.mongoRepository.deleteMany({ userUuid })
|
||||
}
|
||||
|
||||
async findAll(query: ItemQuery): Promise<Item[]> {
|
||||
@@ -136,7 +136,7 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
|
||||
}
|
||||
|
||||
async remove(item: Item): Promise<void> {
|
||||
await this.mongoRepository.deleteOne({ where: { _id: { $eq: BSON.UUID.createFromHexString(item.uuid.value) } } })
|
||||
await this.mongoRepository.deleteOne({ _id: { $eq: BSON.UUID.createFromHexString(item.uuid.value) } })
|
||||
}
|
||||
|
||||
async save(item: Item): Promise<void> {
|
||||
@@ -189,9 +189,7 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
|
||||
}
|
||||
}
|
||||
if (query.deleted !== undefined) {
|
||||
const deletedMixedValues = query.deleted === true ? [true, 1] : [false, 0]
|
||||
|
||||
options.where = { ...options.where, deleted: { $in: deletedMixedValues } }
|
||||
options.where = { ...options.where, deleted: { $eq: query.deleted } }
|
||||
}
|
||||
if (query.contentType) {
|
||||
if (Array.isArray(query.contentType)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user