Compare commits

..

14 Commits

Author SHA1 Message Date
standardci
94448bb5d8 chore(release): publish new version
- @standardnotes/analytics@2.26.0
 - @standardnotes/api-gateway@1.73.4
 - @standardnotes/auth-server@1.137.3
 - @standardnotes/domain-events-infra@1.12.18
 - @standardnotes/domain-events@2.121.0
 - @standardnotes/event-store@1.11.28
 - @standardnotes/files-server@1.22.7
 - @standardnotes/home-server@1.15.18
 - @standardnotes/revisions-server@1.30.8
 - @standardnotes/scheduler-server@1.20.32
 - @standardnotes/syncing-server@1.90.0
 - @standardnotes/websockets-server@1.10.25
2023-09-01 08:56:44 +00:00
Karol Sójko
9a568b0f73 feat: send websocket event to user when a message is sent (#802) 2023-09-01 09:39:10 +02:00
Karol Sójko
a1ee491dc5 fix(revisions): add transition start info 2023-09-01 08:22:29 +02:00
Karol Sójko
e5c118c262 fix(revisions): info logs on total revisions transitioned count 2023-09-01 08:21:41 +02:00
Karol Sójko
1bef1279e6 fix: remove the alive and kicking info logs on workers 2023-09-01 08:19:14 +02:00
Karol Sójko
c511f259c7 fix(analytics): throwing errors on unexisting users 2023-09-01 08:17:22 +02:00
standardci
f77ed8ef94 chore(release): publish new version
- @standardnotes/home-server@1.15.17
 - @standardnotes/revisions-server@1.30.7
2023-08-31 13:57:20 +00:00
Karol Sójko
a4929af2ee fix(revisions): add more verbose messages about failures in revision transitioning 2023-08-31 14:46:11 +02:00
Karol Sójko
095811dda9 fix(revisions): revisions transition check for total count at the end 2023-08-31 14:00:05 +02:00
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
70 changed files with 810 additions and 104 deletions

View File

@@ -3,6 +3,21 @@
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -3,6 +3,16 @@
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

View File

@@ -24,6 +24,4 @@ void container.load().then((container) => {
TYPES.Auth_DomainEventSubscriberFactory,
)
subscriberFactory.create().start()
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
})

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
import { DomainEventInterface } from './DomainEventInterface'
import { MessageSentToUserEventPayload } from './MessageSentToUserEventPayload'
export interface MessageSentToUserEvent extends DomainEventInterface {
type: 'MESSAGE_SENT_TO_USER'
payload: MessageSentToUserEventPayload
}

View File

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

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

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

View File

@@ -3,6 +3,16 @@
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

View File

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

View File

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

View File

@@ -3,6 +3,16 @@
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

View File

@@ -24,6 +24,4 @@ void container.load().then((container) => {
TYPES.Files_DomainEventSubscriberFactory,
)
subscriberFactory.create().start()
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
})

View File

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

View File

@@ -3,6 +3,22 @@
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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.15.14",
"version": "1.15.18",
"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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.30.4",
"version": "1.30.8",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

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
@@ -175,7 +175,7 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up primary database revisions for user 00000000-0000-0000-0000-000000000000: error',
'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',
)
})
})
@@ -220,12 +220,32 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('error')
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()
@@ -245,7 +265,7 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up secondary database revisions for user 00000000-0000-0000-0000-000000000000: error',
'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',
)
})
@@ -273,7 +293,7 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up secondary database revisions for user 00000000-0000-0000-0000-000000000000: error',
'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',
)
})
@@ -351,7 +371,7 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid).toHaveBeenCalledTimes(1)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid).not.toHaveBeenCalled()
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
@@ -368,7 +388,25 @@ describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('error')
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)

View File

@@ -26,6 +26,8 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
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)
@@ -74,15 +76,10 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
private async migrateRevisionsForUser(userUuid: Uuid): Promise<Result<void>> {
try {
const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
this.logger.info(`Total revisions count for user ${userUuid.value} is ${totalRevisionsCountForUser}`)
let totalRevisionsCountTransitionedToSecondary = 0
const pageSize = 1
const totalPages = Math.ceil(totalRevisionsCountForUser / pageSize)
this.logger.info(`Total pages to migrate for user ${userUuid.value} is ${totalPages}`)
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
this.logger.info(`Migrating page ${currentPage} of ${totalPages} for user ${userUuid.value}`)
const query = {
userUuid: userUuid,
offset: (currentPage - 1) * pageSize,
@@ -91,16 +88,28 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
this.logger.info(`Migrating ${revisions.length} revisions for user ${userUuid.value}`)
for (const revision of revisions) {
await (this.secondRevisionsRepository as RevisionRepositoryInterface).save(revision)
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((error as Error).message)
return Result.fail(`Errored when migrating revisions for user ${userUuid.value}: ${(error as Error).message}`)
}
}
@@ -113,7 +122,7 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
return Result.ok()
} catch (error) {
return Result.fail((error as Error).message)
return Result.fail(`Errored when deleting revisions for user ${userUuid.value}: ${(error as Error).message}`)
}
}
@@ -125,15 +134,6 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid: Uuid): Promise<Result<boolean>> {
try {
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
const totalRevisionsCountForUserInSecondary = await (
this.secondRevisionsRepository as RevisionRepositoryInterface
).countByUserUuid(userUuid)
if (totalRevisionsCountForUserInPrimary !== totalRevisionsCountForUserInSecondary) {
return Result.fail(
`Total revisions count for user ${userUuid.value} in primary database (${totalRevisionsCountForUserInPrimary}) does not match total revisions count in secondary database (${totalRevisionsCountForUserInSecondary})`,
)
}
const pageSize = 1
const totalPages = Math.ceil(totalRevisionsCountForUserInPrimary / pageSize)
@@ -167,9 +167,21 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
}
}
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((error as Error).message)
return Result.fail(
`Errored when checking integrity between primary and secondary database: ${(error as Error).message}`,
)
}
}
}

View File

@@ -123,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,
@@ -136,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,16 @@
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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,4 @@ void container.load().then((container) => {
TYPES.Sync_DomainEventSubscriberFactory,
)
subscriberFactory.create().start()
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
})

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.88.2",
"version": "1.90.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)
@@ -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)

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

View File

@@ -3,11 +3,36 @@ import {
EmailRequestedEvent,
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
MessageSentToUserEvent,
NotificationAddedForUserEvent,
RevisionsCopyRequestedEvent,
TransitionStatusUpdatedEvent,
WebSocketMessageRequestedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
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'

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

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

View File

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

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

@@ -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,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.25](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.24...@standardnotes/websockets-server@1.10.25) (2023-09-01)
### Bug Fixes
* remove the alive and kicking info logs on workers ([1bef127](https://github.com/standardnotes/server/commit/1bef1279e6dbf3cbdfa87e44aa9108ed6dbb3b0f))
## [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

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

View File

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