Compare commits

..

17 Commits

Author SHA1 Message Date
standardci
480d5879ba chore(release): publish new version
- @standardnotes/analytics@2.25.21
 - @standardnotes/api-gateway@1.73.3
 - @standardnotes/auth-server@1.137.2
 - @standardnotes/domain-events-infra@1.12.17
 - @standardnotes/domain-events@2.120.0
 - @standardnotes/event-store@1.11.27
 - @standardnotes/files-server@1.22.6
 - @standardnotes/home-server@1.15.16
 - @standardnotes/revisions-server@1.30.6
 - @standardnotes/scheduler-server@1.20.31
 - @standardnotes/syncing-server@1.89.0
 - @standardnotes/websockets-server@1.10.24
2023-08-31 11:58:21 +00:00
Karol Sójko
c0722b173b feat: add sending notifications to user via websockets (#799)
* feat: add sending notifications to user via websockets

* fix: notification added for user event payload structure
2023-08-31 13:26:42 +02:00
standardci
f07c8e4bd4 chore(release): publish new version
- @standardnotes/home-server@1.15.15
 - @standardnotes/revisions-server@1.30.5
 - @standardnotes/syncing-server@1.88.3
2023-08-31 11:26:16 +00:00
Karol Sójko
baf4b2c1d2 fix(syncing-server): persistence mapping for deleted field 2023-08-31 12:11:39 +02:00
Karol Sójko
a6039bd99a fix(revisions): indiciate if revision did not save 2023-08-31 12:04:21 +02:00
standardci
7c0010c902 chore(release): publish new version
- @standardnotes/analytics@2.25.20
 - @standardnotes/api-gateway@1.73.2
 - @standardnotes/auth-server@1.137.1
 - @standardnotes/event-store@1.11.26
 - @standardnotes/files-server@1.22.5
 - @standardnotes/home-server@1.15.14
 - @standardnotes/revisions-server@1.30.4
 - @standardnotes/scheduler-server@1.20.30
 - @standardnotes/syncing-server@1.88.2
 - @standardnotes/time@1.15.1
2023-08-31 09:20:18 +00:00
Karol Sójko
596a0f1a02 fix: transitionining revisions (#801) 2023-08-31 09:50:57 +02:00
standardci
efda3df09b chore(release): publish new version
- @standardnotes/home-server@1.15.13
 - @standardnotes/revisions-server@1.30.3
 - @standardnotes/syncing-server@1.88.1
2023-08-30 17:00:54 +00:00
Karol Sójko
ec35f46d45 fix: mongo delete queries 2023-08-30 18:23:08 +02:00
standardci
c64fa2f47c chore(release): publish new version
- @standardnotes/home-server@1.15.12
 - @standardnotes/revisions-server@1.30.2
2023-08-30 15:51:18 +00:00
Karol Sójko
6ce42a0101 fix(revisions): mongo queries 2023-08-30 17:13:11 +02:00
standardci
d40c74c072 chore(release): publish new version
- @standardnotes/home-server@1.15.11
 - @standardnotes/revisions-server@1.30.1
2023-08-30 14:56:54 +00:00
Karol Sójko
d722206916 fix(revisions): message in logs for trasitions status updated 2023-08-30 16:18:21 +02:00
standardci
19e4c8bf5e chore(release): publish new version
- @standardnotes/api-gateway@1.73.1
 - @standardnotes/home-server@1.15.10
2023-08-30 13:43:55 +00:00
Karol Sójko
ee656b868b fix(api-gateway): transition triggering endpoint call for revisions 2023-08-30 14:08:13 +02:00
standardci
5e79d28bbf chore(release): publish new version
- @standardnotes/api-gateway@1.73.0
 - @standardnotes/home-server@1.15.9
 - @standardnotes/revisions-server@1.30.0
2023-08-30 11:49:34 +00:00
Karol Sójko
25ffd6b803 feat: add a way to trigger transition procedure for revisions (#798)
* feat: add a way to trigger transition procedure for revisions

* fix: localstack linking

* fix: revisions endpoints
2023-08-30 13:14:49 +02:00
67 changed files with 953 additions and 99 deletions

1
.pnp.cjs generated
View File

@@ -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"],\

View File

@@ -178,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"

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.19",
"version": "2.25.21",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.72.3",
"version": "1.73.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -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,
)
}
}

View File

@@ -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'],

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.137.0",
"version": "1.137.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.12.16",
"version": "1.12.17",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.119.0",
"version": "2.120.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -0,0 +1,8 @@
import { DomainEventInterface } from './DomainEventInterface'
import { NotificationAddedForUserEventPayload } from './NotificationAddedForUserEventPayload'
export interface NotificationAddedForUserEvent extends DomainEventInterface {
type: 'NOTIFICATION_ADDED_FOR_USER'
payload: NotificationAddedForUserEventPayload
}

View File

@@ -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
}
}

View File

@@ -42,6 +42,8 @@ export * from './Event/ListedAccountRequestedEvent'
export * from './Event/ListedAccountRequestedEventPayload'
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'

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.11.25",
"version": "1.11.27",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.22.4",
"version": "1.22.6",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.15.8",
"version": "1.15.16",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -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=

View File

@@ -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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.29.0",
"version": "1.30.6",
"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:^",

View File

@@ -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'
@@ -55,6 +58,10 @@ import { TypeORMRevisionRepositoryResolver } from '../Infra/TypeORM/TypeORMRevis
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?: {
@@ -98,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()
@@ -108,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)
@@ -173,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) => {
@@ -234,6 +320,16 @@ export class ContainerConfigLoader {
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'))
@@ -263,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(
@@ -341,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) {
@@ -388,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),
),
)

View File

@@ -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
@@ -38,6 +41,9 @@ const TYPES = {
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'),
@@ -46,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'),

View 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,
}
}
}

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -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()
})
})

View File

@@ -12,5 +12,5 @@ export interface RevisionRepositoryInterface {
findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>>
updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void>
findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Array<Revision>>
save(revision: Revision): Promise<Revision>
save(revision: Revision): Promise<boolean>
}

View File

@@ -96,7 +96,7 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
primaryRevisionRepository.removeByUserUuid = jest.fn().mockResolvedValue(undefined)
secondaryRevisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
secondaryRevisionRepository.save = jest.fn().mockResolvedValue(undefined)
secondaryRevisionRepository.save = jest.fn().mockResolvedValue(true)
secondaryRevisionRepository.removeByUserUuid = jest.fn().mockResolvedValue(undefined)
secondaryRevisionRepository.countByUserUuid = jest.fn().mockResolvedValue(2)
secondaryRevisionRepository.findOneByUuid = jest
@@ -109,6 +109,7 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
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,
@@ -372,5 +373,23 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
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)
})
})
})

View File

@@ -38,6 +38,8 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
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)
@@ -84,7 +86,10 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
for (const revision of revisions) {
await (this.secondRevisionsRepository as RevisionRepositoryInterface).save(revision)
const didSave = await (this.secondRevisionsRepository as RevisionRepositoryInterface).save(revision)
if (!didSave) {
return Result.fail(`Failed to save revision ${revision.id.toString()} to secondary database`)
}
}
}
@@ -107,6 +112,11 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
}
}
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)

View File

@@ -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()
})
})

View File

@@ -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()
}
}

View File

@@ -0,0 +1,3 @@
export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO {
userUuid: string
}

View File

@@ -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)
}
}

View File

@@ -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 })
}
}

View File

@@ -17,7 +17,7 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
) {}
async countByUserUuid(userUuid: Uuid): Promise<number> {
return this.mongoRepository.count({ where: { userUuid: { $eq: userUuid.value } } })
return this.mongoRepository.count({ userUuid: { $eq: userUuid.value } })
}
async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Revision[]> {
@@ -39,17 +39,13 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
}
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 },
})
}
@@ -127,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,
@@ -140,6 +136,6 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
{ upsert: true },
)
return revision
return updateResult.acknowledged
}
}

View File

@@ -106,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>> {

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.20.29",
"version": "1.20.31",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.88.0",
"version": "1.89.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -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)

View File

@@ -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'),

View File

@@ -5,8 +5,10 @@ import {
EmailRequestedEvent,
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
NotificationAddedForUserEvent,
RevisionsCopyRequestedEvent,
TransitionStatusUpdatedEvent,
WebSocketMessageRequestedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
@@ -14,6 +16,45 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(private timer: TimerInterface) {}
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'

View File

@@ -3,11 +3,24 @@ import {
EmailRequestedEvent,
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
NotificationAddedForUserEvent,
RevisionsCopyRequestedEvent,
TransitionStatusUpdatedEvent,
WebSocketMessageRequestedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: string }): WebSocketMessageRequestedEvent
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'

View File

@@ -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()
})
})

View File

@@ -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)
}
}

View File

@@ -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)
})
})

View File

@@ -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()
}
}

View File

@@ -0,0 +1,6 @@
import { DomainEventInterface } from '@standardnotes/domain-events'
export interface SendEventToClientDTO {
userUuid: string
event: DomainEventInterface
}

View File

@@ -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,

View File

@@ -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 })

View File

@@ -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)) {

View File

@@ -99,7 +99,7 @@ export class SQLItemPersistenceMapper implements MapperInterface<Item, SQLItem>
encItemKey: projection.encItemKey,
authHash: projection.authHash,
userUuid,
deleted: projection.deleted,
deleted: !!projection.deleted,
dates,
timestamps,
updatedWithSession,
@@ -127,7 +127,7 @@ export class SQLItemPersistenceMapper implements MapperInterface<Item, SQLItem>
typeorm.encItemKey = domain.props.encItemKey
typeorm.authHash = domain.props.authHash
typeorm.userUuid = domain.props.userUuid.value
typeorm.deleted = domain.props.deleted
typeorm.deleted = !!domain.props.deleted
typeorm.createdAt = domain.props.dates.createdAt
typeorm.updatedAt = domain.props.dates.updatedAt
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt

View File

@@ -64,7 +64,7 @@ export class SQLLegacyItemPersistenceMapper implements MapperInterface<Item, SQL
encItemKey: projection.encItemKey,
authHash: projection.authHash,
userUuid,
deleted: projection.deleted,
deleted: !!projection.deleted,
dates,
timestamps,
updatedWithSession,
@@ -90,7 +90,7 @@ export class SQLLegacyItemPersistenceMapper implements MapperInterface<Item, SQL
typeorm.encItemKey = domain.props.encItemKey
typeorm.authHash = domain.props.authHash
typeorm.userUuid = domain.props.userUuid.value
typeorm.deleted = domain.props.deleted
typeorm.deleted = !!domain.props.deleted
typeorm.createdAt = domain.props.dates.createdAt
typeorm.updatedAt = domain.props.dates.updatedAt
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.15.1](https://github.com/standardnotes/server/compare/@standardnotes/time@1.15.0...@standardnotes/time@1.15.1) (2023-08-31)
### Bug Fixes
* transitionining revisions ([#801](https://github.com/standardnotes/server/issues/801)) ([596a0f1](https://github.com/standardnotes/server/commit/596a0f1a0221ab0636c4c04d17a28c57fe74b620))
# [1.15.0](https://github.com/standardnotes/server/compare/@standardnotes/time@1.14.6...@standardnotes/time@1.15.0) (2023-08-24)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/time",
"version": "1.15.0",
"version": "1.15.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -10,6 +10,11 @@ export class Timer implements TimerInterface {
dayjs.extend(utc)
}
/* istanbul ignore next */
async sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}
getUTCDateNSecondsAhead(n: number): Date {
return dayjs.utc().add(n, 'second').toDate()
}

View File

@@ -23,4 +23,5 @@ export interface TimerInterface {
convertMicrosecondsToTimeStructure(microseconds: number): TimeStructure
formatDate(date: Date, format: string): string
dateWasNDaysAgo(date: Date): number
sleep(milliseconds: number): Promise<void>
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.24](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.23...@standardnotes/websockets-server@1.10.24) (2023-08-31)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.10.23](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.22...@standardnotes/websockets-server@1.10.23) (2023-08-30)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.10.23",
"version": "1.10.24",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -4738,6 +4738,7 @@ __metadata:
resolution: "@standardnotes/revisions-server@workspace:packages/revisions"
dependencies:
"@aws-sdk/client-s3": "npm:^3.332.0"
"@aws-sdk/client-sns": "npm:^3.332.0"
"@aws-sdk/client-sqs": "npm:^3.332.0"
"@newrelic/winston-enricher": "npm:^4.0.1"
"@standardnotes/api": "npm:^1.26.26"