Compare commits

..

18 Commits

Author SHA1 Message Date
standardci 6778a80f21 chore(release): publish new version
- @standardnotes/analytics@2.26.13
 - @standardnotes/api-gateway@1.74.11
 - @standardnotes/auth-server@1.142.0
 - @standardnotes/domain-events-infra@1.12.29
 - @standardnotes/domain-events@2.126.0
 - @standardnotes/event-store@1.11.41
 - @standardnotes/files-server@1.22.20
 - @standardnotes/home-server@1.15.56
 - @standardnotes/revisions-server@1.34.0
 - @standardnotes/scheduler-server@1.20.45
 - @standardnotes/syncing-server@1.96.0
 - @standardnotes/websockets-server@1.10.42
2023-09-15 09:49:09 +00:00
Karol Sójko d4d49454a6 feat: add skipping verified transitions (#827)
* fix(syncing-server): remove transitioning individual users

* feat: add skipping verified transitions

* fix(auth): remove unused use case
2023-09-15 11:31:52 +02:00
standardci 04b52e6773 chore(release): publish new version
- @standardnotes/auth-server@1.141.14
 - @standardnotes/home-server@1.15.55
 - @standardnotes/syncing-server@1.95.19
2023-09-15 08:53:53 +00:00
Karol Sójko 2a1859e4be fix(auth): remove extensive logs from updating transitions 2023-09-15 10:35:50 +02:00
Karol Sójko dd9a9c68cb fix(auth): upgrade simplewebauthn dependency (#826) 2023-09-15 10:35:14 +02:00
Karol Sójko 9147ff5d49 fix(syncing-server): remove unused index in mongodb 2023-09-14 14:18:20 +02:00
standardci 503b84531b chore(release): publish new version
- @standardnotes/auth-server@1.141.13
 - @standardnotes/home-server@1.15.54
 - @standardnotes/revisions-server@1.33.21
 - @standardnotes/syncing-server@1.95.18
2023-09-14 10:16:54 +00:00
Karol Sójko fe8ca828fb fix(auth): set ttl for started and not picked up transitions to 10h 2023-09-14 11:57:21 +02:00
Karol Sójko 03a4a3f2ab fix: skip already updated items and revisions in integrity check (#825) 2023-09-14 11:54:30 +02:00
Karol Sójko 3a8607d146 fix(syncing-server): updating with missing creation date (#824) 2023-09-14 11:45:24 +02:00
standardci 93b6e65554 chore(release): publish new version
- @standardnotes/auth-server@1.141.12
 - @standardnotes/home-server@1.15.53
2023-09-13 10:22:30 +00:00
Karol Sójko 5984e4c3e7 fix(auth): remove re-triggering revisions transition 2023-09-13 11:51:34 +02:00
standardci b4257c10ea chore(release): publish new version
- @standardnotes/auth-server@1.141.11
 - @standardnotes/home-server@1.15.52
2023-09-13 09:41:28 +00:00
Karol Sójko c164bde847 fix(auth): passing transition timestamp 2023-09-13 11:03:32 +02:00
standardci 883df939dd chore(release): publish new version
- @standardnotes/api-gateway@1.74.10
 - @standardnotes/auth-server@1.141.10
 - @standardnotes/home-server@1.15.51
 - @standardnotes/revisions-server@1.33.20
 - @standardnotes/syncing-server@1.95.17
2023-09-13 08:40:17 +00:00
Karol Sójko c7807d0f9e fix: adjust transition timestamps to be universal 2023-09-13 10:25:02 +02:00
standardci fc90343aaa chore(release): publish new version
- @standardnotes/home-server@1.15.50
 - @standardnotes/revisions-server@1.33.19
 - @standardnotes/syncing-server@1.95.16
2023-09-13 08:17:45 +00:00
Karol Sójko fbcb45c3a2 fix: include handling updated items in revisions in secondary 2023-09-13 10:00:02 +02:00
90 changed files with 642 additions and 762 deletions
Generated
+43 -63
View File
@@ -3326,10 +3326,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@hexagon/base64", [\
["npm:1.1.26", {\
"packageLocation": "./.yarn/cache/@hexagon-base64-npm-1.1.26-dbfda05df8-e42582ed12.zip/node_modules/@hexagon/base64/",\
["npm:1.1.27", {\
"packageLocation": "./.yarn/cache/@hexagon-base64-npm-1.1.27-df6f264962-899fffaf54.zip/node_modules/@hexagon/base64/",\
"packageDependencies": [\
["@hexagon/base64", "npm:1.1.26"]\
["@hexagon/base64", "npm:1.1.27"]\
],\
"linkType": "HARD"\
}]\
@@ -5067,44 +5067,29 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/iso-webcrypto", [\
["npm:7.2.0", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-iso-webcrypto-npm-7.2.0-db7b12b859-b57899d0ad.zip/node_modules/@simplewebauthn/iso-webcrypto/",\
"packageDependencies": [\
["@simplewebauthn/iso-webcrypto", "npm:7.2.0"],\
["@simplewebauthn/typescript-types", "npm:7.0.0"],\
["@types/node", "npm:18.16.16"]\
],\
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/server", [\
["npm:7.2.0", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-7.2.0-f1ed5fde8a-2e37c87edd.zip/node_modules/@simplewebauthn/server/",\
["npm:8.1.1", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-8.1.1-106d3bd108-a07c2a067b.zip/node_modules/@simplewebauthn/server/",\
"packageDependencies": [\
["@simplewebauthn/server", "npm:7.2.0"],\
["@hexagon/base64", "npm:1.1.26"],\
["@simplewebauthn/server", "npm:8.1.1"],\
["@hexagon/base64", "npm:1.1.27"],\
["@peculiar/asn1-android", "npm:2.3.6"],\
["@peculiar/asn1-ecc", "npm:2.3.6"],\
["@peculiar/asn1-rsa", "npm:2.3.6"],\
["@peculiar/asn1-schema", "npm:2.3.6"],\
["@peculiar/asn1-x509", "npm:2.3.6"],\
["@simplewebauthn/iso-webcrypto", "npm:7.2.0"],\
["@simplewebauthn/typescript-types", "npm:7.0.0"],\
["@types/debug", "npm:4.1.8"],\
["@types/node", "npm:18.16.16"],\
["cbor-x", "npm:1.5.3"],\
["cross-fetch", "npm:3.1.6"],\
["debug", "virtual:ac3d8e680759ce54399273724d44e041d6c9b73454d191d411a8c44bb27e22f02aaf6ed9d3ad0ac1c298eac4833cff369c9c7b84c573016112c4f84be2cd8543#npm:4.3.4"]\
["@simplewebauthn/typescript-types", "npm:8.0.0"],\
["cbor-x", "npm:1.5.4"],\
["cross-fetch", "npm:4.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/typescript-types", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-7.0.0-cc6ca20415-124238ea18.zip/node_modules/@simplewebauthn/typescript-types/",\
["npm:8.0.0", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-8.0.0-f3b313c27b-21e0b13268.zip/node_modules/@simplewebauthn/typescript-types/",\
"packageDependencies": [\
["@simplewebauthn/typescript-types", "npm:7.0.0"]\
["@simplewebauthn/typescript-types", "npm:8.0.0"]\
],\
"linkType": "HARD"\
}]\
@@ -5864,8 +5849,8 @@ const RAW_RUNTIME_STATE =
["@cbor-extract/cbor-extract-linux-arm64", "npm:2.1.1"],\
["@cbor-extract/cbor-extract-linux-x64", "npm:2.1.1"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
["@simplewebauthn/server", "npm:7.2.0"],\
["@simplewebauthn/typescript-types", "npm:7.0.0"],\
["@simplewebauthn/server", "npm:8.1.1"],\
["@simplewebauthn/typescript-types", "npm:8.0.0"],\
["@standardnotes/api", "npm:1.26.26"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -6720,16 +6705,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/debug", [\
["npm:4.1.8", {\
"packageLocation": "./.yarn/cache/@types-debug-npm-4.1.8-a04e2ca136-9c190e8129.zip/node_modules/@types/debug/",\
"packageDependencies": [\
["@types/debug", "npm:4.1.8"],\
["@types/ms", "npm:0.7.31"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/dotenv", [\
["npm:8.2.0", {\
"packageLocation": "./.yarn/cache/@types-dotenv-npm-8.2.0-f4d0e3d65b-13f90a36f7.zip/node_modules/@types/dotenv/",\
@@ -6956,15 +6931,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/ms", [\
["npm:0.7.31", {\
"packageLocation": "./.yarn/cache/@types-ms-npm-0.7.31-ea3b89342b-cccb52777b.zip/node_modules/@types/ms/",\
"packageDependencies": [\
["@types/ms", "npm:0.7.31"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/newrelic", [\
["npm:9.14.0", {\
"packageLocation": "./.yarn/cache/@types-newrelic-npm-9.14.0-4668da51a1-2ec951bd8f.zip/node_modules/@types/newrelic/",\
@@ -6982,13 +6948,6 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["npm:18.16.16", {\
"packageLocation": "./.yarn/cache/@types-node-npm-18.16.16-8a41330dc3-946bd4d8e6.zip/node_modules/@types/node/",\
"packageDependencies": [\
["@types/node", "npm:18.16.16"]\
],\
"linkType": "HARD"\
}],\
["npm:20.2.5", {\
"packageLocation": "./.yarn/cache/@types-node-npm-20.2.5-0014d2d9ce-55e4f8d08e.zip/node_modules/@types/node/",\
"packageDependencies": [\
@@ -8706,10 +8665,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["cbor-x", [\
["npm:1.5.3", {\
"packageLocation": "./.yarn/cache/cbor-x-npm-1.5.3-1d452dd267-d4df85b339.zip/node_modules/cbor-x/",\
["npm:1.5.4", {\
"packageLocation": "./.yarn/cache/cbor-x-npm-1.5.4-2d5a649a4b-742aea498a.zip/node_modules/cbor-x/",\
"packageDependencies": [\
["cbor-x", "npm:1.5.3"],\
["cbor-x", "npm:1.5.4"],\
["cbor-extract", "npm:2.1.1"]\
],\
"linkType": "HARD"\
@@ -9434,11 +9393,11 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["cross-fetch", [\
["npm:3.1.6", {\
"packageLocation": "./.yarn/cache/cross-fetch-npm-3.1.6-cdb982d446-a8989fca82.zip/node_modules/cross-fetch/",\
["npm:4.0.0", {\
"packageLocation": "./.yarn/cache/cross-fetch-npm-4.0.0-9c67668db4-30e86b703a.zip/node_modules/cross-fetch/",\
"packageDependencies": [\
["cross-fetch", "npm:3.1.6"],\
["node-fetch", "virtual:0f92dfe7f9dc4fd492639d4a5b7805c2b27442bf599fd4f370b22a7966ba078f5d4525e2a8e8af29369f20e1833ed084bd52be59679efaa6c1c6c10cdbcd8baa#npm:2.6.11"]\
["cross-fetch", "npm:4.0.0"],\
["node-fetch", "virtual:9c67668db478e95ba4d6a763bc55027eeff0d22eaf59478017ea07386fc33a3c7b7b625af78aa86a33991a9a500a7aa216e28632de568f02adefd662ef53a42d#npm:2.7.0"]\
],\
"linkType": "HARD"\
}]\
@@ -14174,6 +14133,13 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "SOFT"\
}],\
["npm:2.7.0", {\
"packageLocation": "./.yarn/cache/node-fetch-npm-2.7.0-587d57004e-a3ad788903.zip/node_modules/node-fetch/",\
"packageDependencies": [\
["node-fetch", "npm:2.7.0"]\
],\
"linkType": "SOFT"\
}],\
["npm:3.3.1", {\
"packageLocation": "./.yarn/cache/node-fetch-npm-3.3.1-576511fc5a-1d0c635bdf.zip/node_modules/node-fetch/",\
"packageDependencies": [\
@@ -14197,6 +14163,20 @@ const RAW_RUNTIME_STATE =
"encoding"\
],\
"linkType": "HARD"\
}],\
["virtual:9c67668db478e95ba4d6a763bc55027eeff0d22eaf59478017ea07386fc33a3c7b7b625af78aa86a33991a9a500a7aa216e28632de568f02adefd662ef53a42d#npm:2.7.0", {\
"packageLocation": "./.yarn/__virtual__/node-fetch-virtual-0ec1497d1c/0/cache/node-fetch-npm-2.7.0-587d57004e-a3ad788903.zip/node_modules/node-fetch/",\
"packageDependencies": [\
["node-fetch", "virtual:9c67668db478e95ba4d6a763bc55027eeff0d22eaf59478017ea07386fc33a3c7b7b625af78aa86a33991a9a500a7aa216e28632de568f02adefd662ef53a42d#npm:2.7.0"],\
["@types/encoding", null],\
["encoding", null],\
["whatwg-url", "npm:5.0.0"]\
],\
"packagePeers": [\
"@types/encoding",\
"encoding"\
],\
"linkType": "HARD"\
}]\
]],\
["node-gyp", [\
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
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.
## [2.26.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.12...@standardnotes/analytics@2.26.13) (2023-09-15)
**Note:** Version bump only for package @standardnotes/analytics
## [2.26.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.11...@standardnotes/analytics@2.26.12) (2023-09-13)
**Note:** Version bump only for package @standardnotes/analytics
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.26.12",
"version": "2.26.13",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+10
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.74.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.10...@standardnotes/api-gateway@1.74.11) (2023-09-15)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.74.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.9...@standardnotes/api-gateway@1.74.10) (2023-09-13)
### Bug Fixes
* adjust transition timestamps to be universal ([c7807d0](https://github.com/standardnotes/api-gateway/commit/c7807d0f9e69ce572c4c03ff606375d706f24d9f))
## [1.74.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.8...@standardnotes/api-gateway@1.74.9) (2023-09-13)
**Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.74.9",
"version": "1.74.11",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -80,15 +80,6 @@ export class UsersController extends BaseHttpController {
)
}
@httpGet('/transition-status', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getTransitionStatus(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
response,
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/transition-status'),
)
}
@httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
@@ -43,7 +43,6 @@ export class EndpointResolver implements EndpointResolverInterface {
['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
['[GET]:users/params', 'auth.users.getKeyParams'],
['[GET]:users/transition-status', 'auth.users.transition-status'],
['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'],
['[POST]:auth', 'auth.users.register'],
@@ -59,13 +58,11 @@ export class EndpointResolver implements EndpointResolverInterface {
// Syncing Server
['[POST]:items/sync', 'sync.items.sync'],
['[POST]:items/check-integrity', 'sync.items.check_integrity'],
['[POST]:items/transition', 'sync.items.transition'],
['[GET]:items/:uuid', 'sync.items.get_item'],
// Revisions Controller V2
['[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'],
+37
View File
@@ -3,6 +3,43 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.142.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.14...@standardnotes/auth-server@1.142.0) (2023-09-15)
### Features
* add skipping verified transitions ([#827](https://github.com/standardnotes/server/issues/827)) ([d4d4945](https://github.com/standardnotes/server/commit/d4d49454a68de0acdf440dc202fa14b9743905f6))
## [1.141.14](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.13...@standardnotes/auth-server@1.141.14) (2023-09-15)
### Bug Fixes
* **auth:** remove extensive logs from updating transitions ([2a1859e](https://github.com/standardnotes/server/commit/2a1859e4beff4cc7c4348ebbff8357a8e061bf5e))
* **auth:** upgrade simplewebauthn dependency ([#826](https://github.com/standardnotes/server/issues/826)) ([dd9a9c6](https://github.com/standardnotes/server/commit/dd9a9c68cb431a61700a8e5d8247eb1b505a9d62))
## [1.141.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.12...@standardnotes/auth-server@1.141.13) (2023-09-14)
### Bug Fixes
* **auth:** set ttl for started and not picked up transitions to 10h ([fe8ca82](https://github.com/standardnotes/server/commit/fe8ca828fb37306e0e5056627e67366885e86861))
## [1.141.12](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.11...@standardnotes/auth-server@1.141.12) (2023-09-13)
### Bug Fixes
* **auth:** remove re-triggering revisions transition ([5984e4c](https://github.com/standardnotes/server/commit/5984e4c3e7e550e5ed53805bde1e6dabcbe54da8))
## [1.141.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.10...@standardnotes/auth-server@1.141.11) (2023-09-13)
### Bug Fixes
* **auth:** passing transition timestamp ([c164bde](https://github.com/standardnotes/server/commit/c164bde847b5974e74fd439f0d439526ad439443))
## [1.141.10](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.9...@standardnotes/auth-server@1.141.10) (2023-09-13)
### Bug Fixes
* adjust transition timestamps to be universal ([c7807d0](https://github.com/standardnotes/server/commit/c7807d0f9e69ce572c4c03ff606375d706f24d9f))
## [1.141.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.8...@standardnotes/auth-server@1.141.9) (2023-09-13)
### Bug Fixes
+34 -19
View File
@@ -10,31 +10,52 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { TimerInterface } from '@standardnotes/time'
import { TransitionStatusRepositoryInterface } from '../src/Domain/Transition/TransitionStatusRepositoryInterface'
import { RoleName } from '@standardnotes/domain-core'
const inputArgs = process.argv.slice(2)
const startDateString = inputArgs[0]
const endDateString = inputArgs[1]
const requestTransition = async (
userRepository: UserRepositoryInterface,
transitionStatusRepository: TransitionStatusRepositoryInterface,
userRepository: UserRepositoryInterface,
logger: Logger,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
timer: TimerInterface,
): Promise<void> => {
const startDate = new Date(startDateString)
const endDate = new Date(endDateString)
const users = await userRepository.findAllCreatedBetween(startDate, endDate)
logger.info(`Found ${users.length} users created between ${startDateString} and ${endDateString}`)
const timestamp = timer.getTimestampInMicroseconds()
logger.info(
`[TRANSITION ${timestamp}] Found ${users.length} users created between ${startDateString} and ${endDateString}`,
)
let usersTriggered = 0
for (const user of users) {
const itemsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'items')
const revisionsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'revisions')
const userRoles = await user.roles
const userHasTransitionRole = userRoles.some((role) => role.name === RoleName.NAMES.TransitionUser)
const bothTransitionStatusesAreVerified =
itemsTransitionStatus === 'VERIFIED' && revisionsTransitionStatus === 'VERIFIED'
if (userHasTransitionRole && bothTransitionStatusesAreVerified) {
continue
}
const transitionRequestedEvent = domainEventFactory.createTransitionRequestedEvent({
userUuid: user.uuid,
type: 'items',
timestamp,
})
usersTriggered += 1
@@ -43,22 +64,8 @@ const requestTransition = async (
}
logger.info(
`Triggered transition for ${usersTriggered} users created between ${startDateString} and ${endDateString}`,
`[TRANSITION ${timestamp}] Triggered transition for ${usersTriggered} users created between ${startDateString} and ${endDateString}`,
)
const revisionStatuses = await transitionStatusRepository.getStatuses('revisions')
const failedStatuses = revisionStatuses.filter((status) => status.status === 'FAILED')
logger.info(`Found ${failedStatuses.length} failed revision transitions`)
for (const status of failedStatuses) {
const transitionRequestedEvent = domainEventFactory.createTransitionRequestedEvent({
userUuid: status.userUuid,
type: 'revisions',
})
await domainEventPublisher.publish(transitionRequestedEvent)
}
}
const container = new ContainerConfigLoader('worker')
@@ -75,12 +82,20 @@ void container.load().then((container) => {
const userRepository: UserRepositoryInterface = container.get(TYPES.Auth_UserRepository)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
const transitionStatusRepository: TransitionStatusRepositoryInterface = container.get(
const timer = container.get<TimerInterface>(TYPES.Auth_Timer)
const transitionStatusRepository = container.get<TransitionStatusRepositoryInterface>(
TYPES.Auth_TransitionStatusRepository,
)
Promise.resolve(
requestTransition(userRepository, transitionStatusRepository, logger, domainEventFactory, domainEventPublisher),
requestTransition(
transitionStatusRepository,
userRepository,
logger,
domainEventFactory,
domainEventPublisher,
timer,
),
)
.then(() => {
logger.info(`Finished transition request for users created between ${startDateString} and ${endDateString}`)
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.141.9",
"version": "1.142.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -40,8 +40,8 @@
"@aws-sdk/client-sqs": "^3.332.0",
"@cbor-extract/cbor-extract-linux-arm64": "^2.1.1",
"@cbor-extract/cbor-extract-linux-x64": "^2.1.1",
"@simplewebauthn/server": "^7.2.0",
"@simplewebauthn/typescript-types": "^7.0.0",
"@simplewebauthn/server": "^8.1.1",
"@simplewebauthn/typescript-types": "^8.0.0",
"@standardnotes/api": "^1.26.26",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
-11
View File
@@ -263,7 +263,6 @@ import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionS
import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
import { SharedVaultUserPersistenceMapper } from '../Mapping/SharedVaultUserPersistenceMapper'
import { SharedVaultUserRepositoryInterface } from '../Domain/SharedVault/SharedVaultUserRepositoryInterface'
@@ -943,15 +942,6 @@ export class ContainerConfigLoader {
new UpdateTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus)
.toConstantValue(
new GetTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
),
)
container
@@ -1276,7 +1266,6 @@ export class ContainerConfigLoader {
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
container.get<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
),
)
-1
View File
@@ -159,7 +159,6 @@ const TYPES = {
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'),
// Handlers
@@ -33,7 +33,11 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
createTransitionRequestedEvent(dto: { userUuid: string; type: 'items' | 'revisions' }): TransitionRequestedEvent {
createTransitionRequestedEvent(dto: {
userUuid: string
type: 'items' | 'revisions'
timestamp: number
}): TransitionRequestedEvent {
return {
type: 'TRANSITION_REQUESTED',
createdAt: this.timer.getUTCDate(),
@@ -44,10 +48,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
},
origin: DomainEventService.Auth,
},
payload: {
timestamp: this.timer.getTimestampInMicroseconds(),
...dto,
},
payload: dto,
}
}
@@ -90,5 +90,9 @@ export interface DomainEventFactoryInterface {
}): StatisticPersistenceRequestedEvent
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent
createSessionRefreshedEvent(dto: { userUuid: string }): SessionRefreshedEvent
createTransitionRequestedEvent(dto: { userUuid: string; type: 'items' | 'revisions' }): TransitionRequestedEvent
createTransitionRequestedEvent(dto: {
userUuid: string
type: 'items' | 'revisions'
timestamp: number
}): TransitionRequestedEvent
}
@@ -2,14 +2,10 @@ export interface TransitionStatusRepositoryInterface {
updateStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED',
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED',
): Promise<void>
removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void>
getStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null>
getStatuses(
transitionType: 'items' | 'revisions',
): Promise<Array<{ userUuid: string; status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' }>>
): Promise<'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null>
}
@@ -35,7 +35,7 @@ export class GenerateAuthenticatorAuthenticationOptions
.update(`u2f-selector-${dto.username}${this.pseudoKeyParamsKey}`)
.digest('base64url')
const options = generateAuthenticationOptions({
const options = await generateAuthenticationOptions({
allowCredentials: [
{
id: Buffer.from(credentialIdHash),
@@ -56,7 +56,7 @@ export class GenerateAuthenticatorAuthenticationOptions
const userUuid = userUuidOrError.getValue()
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
const options = generateAuthenticationOptions({
const options = await generateAuthenticationOptions({
allowCredentials: authenticators.map((authenticator) => ({
id: authenticator.props.credentialId,
type: 'public-key',
@@ -52,7 +52,7 @@ export class GenerateAuthenticatorRegistrationOptions
}
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
const options = generateRegistrationOptions({
const options = await generateRegistrationOptions({
rpID: this.relyingPartyId,
rpName: this.relyingPartyName,
userID: userUuid.value,
@@ -1,114 +0,0 @@
import { RoleName } from '@standardnotes/domain-core'
import { Role } from '../../Role/Role'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GetTransitionStatus } from './GetTransitionStatus'
describe('GetTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface
let userRepository: UserRepositoryInterface
let user: User
let role: Role
const createUseCase = () => new GetTransitionStatus(transitionStatusRepository, userRepository)
beforeEach(() => {
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.getStatus = jest.fn().mockReturnValue(null)
role = {} as jest.Mocked<Role>
role.name = RoleName.NAMES.CoreUser
user = {
uuid: '00000000-0000-0000-0000-000000000000',
email: 'test@test.te',
} as jest.Mocked<User>
user.roles = Promise.resolve([role])
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
})
it('returns transition status FINISHED', async () => {
role.name = RoleName.NAMES.TransitionUser
user.roles = Promise.resolve([role])
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
transitionType: 'items',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('FINISHED')
})
it('returns transition status STARTED', async () => {
const useCase = createUseCase()
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
transitionType: 'items',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('STARTED')
})
it('returns transition status TO-DO', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
transitionType: 'items',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('TO-DO')
})
it('returns transition status FAILED', async () => {
const useCase = createUseCase()
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('FAILED')
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
transitionType: 'items',
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('FAILED')
})
it('return error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
transitionType: 'items',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
it('return error if user not found', async () => {
const useCase = createUseCase()
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
transitionType: 'items',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('User not found.')
})
})
@@ -1,43 +0,0 @@
import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { GetTransitionStatusDTO } from './GetTransitionStatusDTO'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
export class GetTransitionStatus
implements UseCaseInterface<'TO-DO' | 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'>
{
constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface,
private userRepository: UserRepositoryInterface,
) {}
async execute(
dto: GetTransitionStatusDTO,
): Promise<Result<'TO-DO' | 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
return Result.fail('User not found.')
}
const roles = await user.roles
for (const role of roles) {
if (role.name === RoleName.NAMES.TransitionUser) {
return Result.ok('FINISHED')
}
}
const transitionStatus = await this.transitionStatusRepository.getStatus(userUuid.value, dto.transitionType)
if (transitionStatus === null) {
return Result.ok('TO-DO')
}
return Result.ok(transitionStatus)
}
}
@@ -1,4 +0,0 @@
export interface GetTransitionStatusDTO {
userUuid: string
transitionType: 'items' | 'revisions'
}
@@ -3,80 +3,38 @@ import { RoleName, Uuid } from '@standardnotes/domain-core'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatus } from './UpdateTransitionStatus'
import { Logger } from 'winston'
describe('UpdateTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface
let roleService: RoleServiceInterface
let logger: Logger
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService, logger)
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.removeStatus = jest.fn()
transitionStatusRepository.updateStatus = jest.fn()
transitionStatusRepository.getStatuses = jest.fn().mockResolvedValue([
{
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'STARTED',
},
{
userUuid: '00000000-0000-0000-0000-000000000001',
status: 'IN_PROGRESS',
},
{
userUuid: '00000000-0000-0000-0000-000000000002',
status: 'FAILED',
},
])
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addRoleToUser = jest.fn()
})
it('should remove transition status and add TransitionUser role', async () => {
it('should add TRANSITION_USER role', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'FINISHED',
status: 'VERIFIED',
transitionType: 'items',
transitionTimestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000',
'items',
)
expect(roleService.addRoleToUser).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
)
})
it('should remove transition status', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'FINISHED',
transitionType: 'revisions',
transitionTimestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000',
'revisions',
)
expect(roleService.addRoleToUser).not.toHaveBeenCalled()
})
it('should update transition status', async () => {
const useCase = createUseCase()
@@ -2,13 +2,11 @@ import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatusDTO } from './UpdateTransitionStatusDTO'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { Logger } from 'winston'
export class UpdateTransitionStatus implements UseCaseInterface<void> {
constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface,
private roleService: RoleServiceInterface,
private logger: Logger,
) {}
async execute(dto: UpdateTransitionStatusDTO): Promise<Result<void>> {
@@ -18,36 +16,12 @@ export class UpdateTransitionStatus implements UseCaseInterface<void> {
}
const userUuid = userUuidOrError.getValue()
if (dto.status !== 'FINISHED') {
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
return Result.ok()
}
await this.transitionStatusRepository.removeStatus(dto.userUuid, dto.transitionType)
if (dto.transitionType === 'items') {
if (dto.transitionType === 'items' && dto.status === 'VERIFIED') {
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
}
const itemStatuses = await this.transitionStatusRepository.getStatuses('items')
const itemsStartedStatusesCount = itemStatuses.filter((status) => status.status === 'STARTED').length
const itemsInProgressStatusesCount = itemStatuses.filter((status) => status.status === 'IN_PROGRESS').length
const itemsFailedStatusesCount = itemStatuses.filter((status) => status.status === 'FAILED').length
this.logger.info(
`[TRANSITION ${dto.transitionTimestamp}] Items transition statuses: ${itemsStartedStatusesCount} started, ${itemsInProgressStatusesCount} in progress, ${itemsFailedStatusesCount} failed`,
)
const revisionStatuses = await this.transitionStatusRepository.getStatuses('revisions')
const revisionsStartedStatusesCount = revisionStatuses.filter((status) => status.status === 'STARTED').length
const revisionsInProgressStatusesCount = revisionStatuses.filter((status) => status.status === 'IN_PROGRESS').length
const revisionsFailedStatusesCount = revisionStatuses.filter((status) => status.status === 'FAILED').length
this.logger.info(
`[TRANSITION ${dto.transitionTimestamp}] Revisions transition statuses: ${revisionsStartedStatusesCount} started, ${revisionsInProgressStatusesCount} in progress, ${revisionsFailedStatusesCount} failed`,
)
return Result.ok()
}
}
@@ -2,5 +2,5 @@ export interface UpdateTransitionStatusDTO {
userUuid: string
transitionType: 'items' | 'revisions'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED'
}
@@ -4,24 +4,6 @@ export class InMemoryTransitionStatusRepository implements TransitionStatusRepos
private itemStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
private revisionStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
async getStatuses(
transitionType: 'items' | 'revisions',
): Promise<{ userUuid: string; status: 'STARTED' | 'FAILED' | 'IN_PROGRESS' }[]> {
const statuses: { userUuid: string; status: 'STARTED' | 'FAILED' | 'IN_PROGRESS' }[] = []
if (transitionType === 'items') {
for (const [userUuid, status] of this.itemStatuses) {
statuses.push({ userUuid, status })
}
} else {
for (const [userUuid, status] of this.revisionStatuses) {
statuses.push({ userUuid, status })
}
}
return statuses
}
async updateStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
@@ -14,7 +14,6 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
import { User } from '../../Domain/User/User'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
describe('AnnotatedUsersController', () => {
let updateUser: UpdateUser
@@ -25,7 +24,6 @@ describe('AnnotatedUsersController', () => {
let increaseLoginAttempts: IncreaseLoginAttempts
let changeCredentials: ChangeCredentials
let inviteToSharedSubscription: InviteToSharedSubscription
let getTransitionStatus: GetTransitionStatus
let request: express.Request
let response: express.Response
@@ -40,7 +38,6 @@ describe('AnnotatedUsersController', () => {
clearLoginAttempts,
increaseLoginAttempts,
changeCredentials,
getTransitionStatus,
)
beforeEach(() => {
@@ -72,9 +69,6 @@ describe('AnnotatedUsersController', () => {
inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription>
inviteToSharedSubscription.execute = jest.fn()
getTransitionStatus = {} as jest.Mocked<GetTransitionStatus>
getTransitionStatus.execute = jest.fn()
request = {
headers: {},
body: {},
@@ -18,7 +18,6 @@ import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { BaseUsersController } from './Base/BaseUsersController'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
@controller('/users')
export class AnnotatedUsersController extends BaseUsersController {
@@ -30,7 +29,6 @@ export class AnnotatedUsersController extends BaseUsersController {
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
@inject(TYPES.Auth_GetTransitionStatus) override getTransitionStatusUseCase: GetTransitionStatus,
) {
super(
updateUser,
@@ -40,7 +38,6 @@ export class AnnotatedUsersController extends BaseUsersController {
clearLoginAttempts,
increaseLoginAttempts,
changeCredentialsUseCase,
getTransitionStatusUseCase,
)
}
@@ -54,11 +51,6 @@ export class AnnotatedUsersController extends BaseUsersController {
return super.keyParams(request)
}
@httpGet('/transition-status', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async transitionStatus(request: Request, response: Response): Promise<results.JsonResult> {
return super.transitionStatus(request, response)
}
@httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
return super.deleteAccount(request, response)
@@ -10,7 +10,6 @@ import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
import { ErrorTag } from '@standardnotes/responses'
import { GetTransitionStatus } from '../../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
export class BaseUsersController extends BaseHttpController {
constructor(
@@ -21,7 +20,6 @@ export class BaseUsersController extends BaseHttpController {
protected clearLoginAttempts: ClearLoginAttempts,
protected increaseLoginAttempts: IncreaseLoginAttempts,
protected changeCredentialsUseCase: ChangeCredentials,
protected getTransitionStatusUseCase: GetTransitionStatus,
private controllerContainer?: ControllerContainerInterface,
) {
super()
@@ -32,7 +30,6 @@ export class BaseUsersController extends BaseHttpController {
this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
this.controllerContainer.register('auth.users.delete', this.deleteAccount.bind(this))
this.controllerContainer.register('auth.users.transition-status', this.transitionStatus.bind(this))
}
}
@@ -106,30 +103,6 @@ export class BaseUsersController extends BaseHttpController {
return this.json(result.keyParams)
}
async transitionStatus(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.getTransitionStatusUseCase.execute({
userUuid: response.locals.user.uuid,
transitionType: 'items',
})
if (result.isFailed()) {
return this.json(
{
error: {
message: result.getError(),
},
},
400,
)
}
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
return this.json({
status: result.getValue(),
})
}
async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
@@ -7,47 +7,35 @@ export class RedisTransitionStatusRepository implements TransitionStatusReposito
constructor(private redisClient: IORedis.Redis) {}
async getStatuses(
transitionType: 'items' | 'revisions',
): Promise<{ userUuid: string; status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' }[]> {
const keys = await this.redisClient.keys(`${this.PREFIX}:${transitionType}:*`)
const statuses = await Promise.all(
keys.map(async (key) => {
const userUuid = key.split(':')[2]
const status = (await this.redisClient.get(key)) as 'STARTED' | 'IN_PROGRESS' | 'FAILED'
return { userUuid, status }
}),
)
return statuses
}
async updateStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED',
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED',
): Promise<void> {
if (status === 'IN_PROGRESS') {
await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, 7200, status)
} else {
await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status)
switch (status) {
case 'FAILED':
case 'VERIFIED':
await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status)
break
case 'IN_PROGRESS': {
const ttl2Hourse = 7_200
await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, ttl2Hourse, status)
break
}
default: {
const ttl10Hours = 36_000
await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, ttl10Hours, status)
break
}
}
}
async removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void> {
await this.redisClient.del(`${this.PREFIX}:${transitionType}:${userUuid}`)
}
async getStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null> {
const status = (await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)) as
| 'STARTED'
| 'IN_PROGRESS'
| 'FAILED'
| null
): Promise<'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null> {
const status = await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)
return status
return status as 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null
}
}
@@ -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.29](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.28...@standardnotes/domain-events-infra@1.12.29) (2023-09-15)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.28](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.27...@standardnotes/domain-events-infra@1.12.28) (2023-09-13)
**Note:** Version bump only for package @standardnotes/domain-events-infra
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.12.28",
"version": "1.12.29",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+6
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.126.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.125.4...@standardnotes/domain-events@2.126.0) (2023-09-15)
### Features
* add skipping verified transitions ([#827](https://github.com/standardnotes/server/issues/827)) ([d4d4945](https://github.com/standardnotes/server/commit/d4d49454a68de0acdf440dc202fa14b9743905f6))
## [2.125.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.125.3...@standardnotes/domain-events@2.125.4) (2023-09-13)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.125.4",
"version": "2.126.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -2,5 +2,5 @@ export interface TransitionStatusUpdatedEventPayload {
userUuid: string
transitionType: 'items' | 'revisions'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED'
}
+4
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.11.41](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.40...@standardnotes/event-store@1.11.41) (2023-09-15)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.40](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.39...@standardnotes/event-store@1.11.40) (2023-09-13)
**Note:** Version bump only for package @standardnotes/event-store
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.11.40",
"version": "1.11.41",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",
+4
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.22.20](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.19...@standardnotes/files-server@1.22.20) (2023-09-15)
**Note:** Version bump only for package @standardnotes/files-server
## [1.22.19](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.18...@standardnotes/files-server@1.22.19) (2023-09-13)
**Note:** Version bump only for package @standardnotes/files-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.22.19",
"version": "1.22.20",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.15.56](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.55...@standardnotes/home-server@1.15.56) (2023-09-15)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.55](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.54...@standardnotes/home-server@1.15.55) (2023-09-15)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.54](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.53...@standardnotes/home-server@1.15.54) (2023-09-14)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.53](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.52...@standardnotes/home-server@1.15.53) (2023-09-13)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.52](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.51...@standardnotes/home-server@1.15.52) (2023-09-13)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.51](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.50...@standardnotes/home-server@1.15.51) (2023-09-13)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.50](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.49...@standardnotes/home-server@1.15.50) (2023-09-13)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.49](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.48...@standardnotes/home-server@1.15.49) (2023-09-13)
**Note:** Version bump only for package @standardnotes/home-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.15.49",
"version": "1.15.56",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+24
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.34.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.21...@standardnotes/revisions-server@1.34.0) (2023-09-15)
### Features
* add skipping verified transitions ([#827](https://github.com/standardnotes/server/issues/827)) ([d4d4945](https://github.com/standardnotes/server/commit/d4d49454a68de0acdf440dc202fa14b9743905f6))
## [1.33.21](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.20...@standardnotes/revisions-server@1.33.21) (2023-09-14)
### Bug Fixes
* skip already updated items and revisions in integrity check ([#825](https://github.com/standardnotes/server/issues/825)) ([03a4a3f](https://github.com/standardnotes/server/commit/03a4a3f2abc0b4e09942ba39dbd227524068dfb6))
## [1.33.20](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.19...@standardnotes/revisions-server@1.33.20) (2023-09-13)
### Bug Fixes
* adjust transition timestamps to be universal ([c7807d0](https://github.com/standardnotes/server/commit/c7807d0f9e69ce572c4c03ff606375d706f24d9f))
## [1.33.19](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.18...@standardnotes/revisions-server@1.33.19) (2023-09-13)
### Bug Fixes
* include handling updated items in revisions in secondary ([fbcb45c](https://github.com/standardnotes/server/commit/fbcb45c3a23fde09702fae7bfcb409bdbb610191))
## [1.33.18](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.17...@standardnotes/revisions-server@1.33.18) (2023-09-13)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.33.18",
"version": "1.34.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -520,9 +520,6 @@ 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),
),
)
@@ -9,7 +9,8 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: {
userUuid: string
transitionType: 'items' | 'revisions'
status: 'STARTED' | 'FAILED' | 'FINISHED'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent {
return {
type: 'TRANSITION_STATUS_UPDATED',
@@ -19,12 +20,9 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.SyncingServer,
},
payload: {
transitionTimestamp: this.timer.getTimestampInMicroseconds(),
...dto,
origin: DomainEventService.Revisions,
},
payload: dto,
}
}
}
@@ -4,6 +4,7 @@ export interface DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: {
userUuid: string
transitionType: 'items' | 'revisions'
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent
}
@@ -18,6 +18,7 @@ export class TransitionRequestedEventHandler implements DomainEventHandlerInterf
const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
userUuid: event.payload.userUuid,
transitionTimestamp: event.payload.timestamp,
})
if (result.isFailed()) {
@@ -25,6 +25,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid,
status: 'STARTED',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
@@ -32,28 +33,18 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
}
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'revisions') {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.error(
`Failed to transition revisions for user ${event.payload.userUuid}: ${userUuidOrError.getError()}`,
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'FAILED',
transitionType: 'revisions',
}),
)
const userUuid = await this.getUserUuidFromEvent(event)
if (userUuid === null) {
return
}
if (await this.isAlreadyMigrated(userUuidOrError.getValue())) {
if (await this.isAlreadyMigrated(userUuid)) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'FINISHED',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
@@ -65,6 +56,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid,
status: 'IN_PROGRESS',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
@@ -80,6 +72,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid,
status: 'FAILED',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
@@ -91,11 +84,28 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid,
status: 'FINISHED',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
return
}
if (event.payload.status === 'FINISHED' && event.payload.transitionType === 'revisions') {
const userUuid = await this.getUserUuidFromEvent(event)
if (userUuid === null) {
return
}
if (await this.isAlreadyMigrated(userUuid)) {
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'VERIFIED',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
})
}
}
}
private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> {
@@ -109,4 +119,25 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
return totalRevisionsCountForUserInPrimary === 0
}
private async getUserUuidFromEvent(event: TransitionStatusUpdatedEvent): Promise<Uuid | null> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.error(
`Failed to transition revisions for user ${event.payload.userUuid}: ${userUuidOrError.getError()}`,
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'FAILED',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
return null
}
return userUuidOrError.getValue()
}
}
@@ -30,9 +30,12 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
const userUuid = userUuidOrError.getValue()
let newRevisionsInSecondaryCount = 0
let updatedRevisionsInSecondary: Revision[] = []
if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
const newRevisions = await this.getNewRevisionsCreatedInSecondaryDatabase(userUuid)
for (const existingRevision of newRevisions.alreadyExistingInPrimary) {
const { alreadyExistingInPrimary, newRevisionsInSecondary, updatedInSecondary } =
await this.getNewRevisionsCreatedInSecondaryDatabase(userUuid)
for (const existingRevision of alreadyExistingInPrimary) {
this.logger.info(`Removing revision ${existingRevision.id.toString()} from secondary database`)
await (this.secondRevisionsRepository as RevisionRepositoryInterface).removeOneByUuid(
Uuid.create(existingRevision.id.toString()).getValue(),
@@ -40,24 +43,34 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
)
}
if (newRevisions.newRevisionsInSecondary.length > 0) {
if (newRevisionsInSecondary.length > 0) {
this.logger.info(
`Found ${newRevisions.newRevisionsInSecondary.length} new revisions in secondary database for user ${userUuid.value}`,
`Found ${newRevisionsInSecondary.length} new revisions in secondary database for user ${userUuid.value}`,
)
}
newRevisionsInSecondaryCount = newRevisions.newRevisionsInSecondary.length
newRevisionsInSecondaryCount = newRevisionsInSecondary.length
if (updatedInSecondary.length > 0) {
this.logger.info(
`Found ${updatedInSecondary.length} updated revisions in secondary database for user ${userUuid.value}`,
)
}
updatedRevisionsInSecondary = updatedInSecondary
}
const updatedRevisionsInSecondaryCount = updatedRevisionsInSecondary.length
await this.allowForSecondaryDatabaseToCatchUp()
const migrationTimeStart = this.timer.getTimestampInMicroseconds()
this.logger.debug(`Transitioning revisions for user ${userUuid.value}`)
const migrationResult = await this.migrateRevisionsForUser(userUuid)
const migrationResult = await this.migrateRevisionsForUser(userUuid, updatedRevisionsInSecondary)
if (migrationResult.isFailed()) {
if (newRevisionsInSecondaryCount === 0) {
if (newRevisionsInSecondaryCount === 0 && updatedRevisionsInSecondaryCount === 0) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
@@ -74,9 +87,10 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid,
newRevisionsInSecondaryCount,
updatedRevisionsInSecondary,
)
if (integrityCheckResult.isFailed()) {
if (newRevisionsInSecondaryCount === 0) {
if (newRevisionsInSecondaryCount === 0 && updatedRevisionsInSecondaryCount === 0) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
@@ -107,7 +121,10 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
return Result.ok()
}
private async migrateRevisionsForUser(userUuid: Uuid): Promise<Result<void>> {
private async migrateRevisionsForUser(
userUuid: Uuid,
updatedRevisionsInSecondary: Revision[],
): Promise<Result<void>> {
try {
const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
let totalRevisionsCountTransitionedToSecondary = 0
@@ -123,6 +140,18 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
for (const revision of revisions) {
try {
if (
updatedRevisionsInSecondary.find(
(updatedRevision) => updatedRevision.id.toString() === revision.id.toString(),
)
) {
this.logger.info(
`Skipping saving revision ${revision.id.toString()} as it was updated in secondary database`,
)
continue
}
this.logger.debug(
`Transitioning revision #${
totalRevisionsCountTransitionedToSecondary + 1
@@ -188,6 +217,7 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
private async getNewRevisionsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
alreadyExistingInPrimary: Revision[]
newRevisionsInSecondary: Revision[]
updatedInSecondary: Revision[]
}> {
const revisions = await (this.secondRevisionsRepository as RevisionRepositoryInterface).findByUserUuid({
userUuid: userUuid,
@@ -195,23 +225,35 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
const alreadyExistingInPrimary: Revision[] = []
const newRevisionsInSecondary: Revision[] = []
const updatedInSecondary: Revision[] = []
for (const revision of revisions) {
const revisionExistsInPrimary = await this.checkIfRevisionExistsInPrimaryDatabase(revision)
if (revisionExistsInPrimary) {
const { revisionInPrimary, newerRevisionInSecondary } =
await this.checkIfRevisionExistsInPrimaryDatabase(revision)
if (revisionInPrimary !== null) {
alreadyExistingInPrimary.push(revision)
} else {
continue
}
if (newerRevisionInSecondary !== null) {
updatedInSecondary.push(newerRevisionInSecondary)
continue
}
if (revisionInPrimary === null && newerRevisionInSecondary === null) {
newRevisionsInSecondary.push(revision)
continue
}
}
return {
alreadyExistingInPrimary: alreadyExistingInPrimary,
newRevisionsInSecondary: newRevisionsInSecondary,
updatedInSecondary: updatedInSecondary,
}
}
private async checkIfRevisionExistsInPrimaryDatabase(revision: Revision): Promise<boolean> {
private async checkIfRevisionExistsInPrimaryDatabase(
revision: Revision,
): Promise<{ revisionInPrimary: Revision | null; newerRevisionInSecondary: Revision | null }> {
const revisionInPrimary = await this.primaryRevisionsRepository.findOneByUuid(
Uuid.create(revision.id.toString()).getValue(),
revision.props.userUuid as Uuid,
@@ -219,7 +261,10 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
)
if (revisionInPrimary === null) {
return false
return {
revisionInPrimary: null,
newerRevisionInSecondary: null,
}
}
if (!revision.isIdenticalTo(revisionInPrimary)) {
@@ -229,15 +274,23 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
)}, revision in primary database: ${JSON.stringify(revisionInPrimary)}`,
)
return false
return {
revisionInPrimary: null,
newerRevisionInSecondary:
revision.props.dates.updatedAt > revisionInPrimary.props.dates.updatedAt ? revision : null,
}
}
return true
return {
revisionInPrimary: revisionInPrimary,
newerRevisionInSecondary: null,
}
}
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid: Uuid,
newRevisionsInSecondaryCount: number,
updatedRevisionsInSecondary: Revision[],
): Promise<Result<boolean>> {
try {
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
@@ -267,6 +320,17 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
return Result.fail(`Revision ${revision.id.toString()} not found in secondary database`)
}
if (
updatedRevisionsInSecondary.find(
(updatedRevision) => updatedRevision.id.toString() === revision.id.toString(),
)
) {
this.logger.info(
`Skipping integrity check for revision ${revision.id.toString()} as it was updated in secondary database`,
)
continue
}
if (!revision.isIdenticalTo(revisionInSecondary)) {
return Result.fail(
`Revision ${revision.id.toString()} is not identical in primary and secondary database. Revision in primary database: ${JSON.stringify(
@@ -23,6 +23,7 @@ describe('TriggerTransitionFromPrimaryToSecondaryDatabaseForUser', () => {
await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
transitionTimestamp: 123,
})
expect(domainEventPubliser.publish).toHaveBeenCalled()
@@ -15,6 +15,7 @@ export class TriggerTransitionFromPrimaryToSecondaryDatabaseForUser implements U
userUuid: dto.userUuid,
status: 'STARTED',
transitionType: 'revisions',
transitionTimestamp: dto.transitionTimestamp,
})
await this.domainEventPubliser.publish(event)
@@ -1,3 +1,4 @@
export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO {
userUuid: string
transitionTimestamp: number
}
@@ -1,5 +1,5 @@
import { Request, Response } from 'express'
import { controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils'
import { controller, httpDelete, httpGet, results } from 'inversify-express-utils'
import { inject } from 'inversify'
import TYPES from '../../Bootstrap/Types'
@@ -12,7 +12,6 @@ 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('', TYPES.Revisions_ApiGatewayAuthMiddleware)
export class AnnotatedRevisionsController extends BaseRevisionsController {
@@ -24,17 +23,8 @@ 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,
triggerTransitionFromPrimaryToSecondaryDatabaseForUser,
)
super(getRevisionsMetadata, doGetRevision, doDeleteRevision, revisionHttpMapper, revisionMetadataHttpMapper)
}
@httpGet('/items/:itemUuid/revisions')
@@ -51,9 +41,4 @@ export class AnnotatedRevisionsController extends BaseRevisionsController {
override async deleteRevision(request: Request, response: Response): Promise<results.JsonResult> {
return super.deleteRevision(request, response)
}
@httpPost('/revisions/transition')
override async transition(request: Request, response: Response): Promise<results.JsonResult> {
return super.transition(request, response)
}
}
@@ -11,7 +11,6 @@ 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(
@@ -20,7 +19,6 @@ 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()
@@ -29,7 +27,6 @@ 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))
}
}
@@ -108,23 +105,4 @@ 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 })
}
}
+4
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.20.45](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.44...@standardnotes/scheduler-server@1.20.45) (2023-09-15)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.44](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.43...@standardnotes/scheduler-server@1.20.44) (2023-09-13)
**Note:** Version bump only for package @standardnotes/scheduler-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.20.44",
"version": "1.20.45",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+31
View File
@@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.96.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.19...@standardnotes/syncing-server@1.96.0) (2023-09-15)
### Features
* add skipping verified transitions ([#827](https://github.com/standardnotes/syncing-server-js/issues/827)) ([d4d4945](https://github.com/standardnotes/syncing-server-js/commit/d4d49454a68de0acdf440dc202fa14b9743905f6))
## [1.95.19](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.18...@standardnotes/syncing-server@1.95.19) (2023-09-15)
### Bug Fixes
* **syncing-server:** remove unused index in mongodb ([9147ff5](https://github.com/standardnotes/syncing-server-js/commit/9147ff5d49c507d943f4f8c6775f7c1fff878b0f))
## [1.95.18](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.17...@standardnotes/syncing-server@1.95.18) (2023-09-14)
### Bug Fixes
* skip already updated items and revisions in integrity check ([#825](https://github.com/standardnotes/syncing-server-js/issues/825)) ([03a4a3f](https://github.com/standardnotes/syncing-server-js/commit/03a4a3f2abc0b4e09942ba39dbd227524068dfb6))
* **syncing-server:** updating with missing creation date ([#824](https://github.com/standardnotes/syncing-server-js/issues/824)) ([3a8607d](https://github.com/standardnotes/syncing-server-js/commit/3a8607d1465cabedad68b84c753e407342e60d20))
## [1.95.17](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.16...@standardnotes/syncing-server@1.95.17) (2023-09-13)
### Bug Fixes
* adjust transition timestamps to be universal ([c7807d0](https://github.com/standardnotes/syncing-server-js/commit/c7807d0f9e69ce572c4c03ff606375d706f24d9f))
## [1.95.16](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.15...@standardnotes/syncing-server@1.95.16) (2023-09-13)
### Bug Fixes
* include handling updated items in revisions in secondary ([fbcb45c](https://github.com/standardnotes/syncing-server-js/commit/fbcb45c3a23fde09702fae7bfcb409bdbb610191))
## [1.95.15](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.14...@standardnotes/syncing-server@1.95.15) (2023-09-13)
### Bug Fixes
-50
View File
@@ -1,50 +0,0 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../src/Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
const inputArgs = process.argv.slice(2)
const userUuid = inputArgs[0]
const requestTransition = async (
triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
logger: Logger,
): Promise<void> => {
const result = await triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
userUuid,
})
if (result.isFailed()) {
logger.error(`Could not trigger transition for user ${userUuid}: ${result.getError()}`)
}
return
}
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Sync_Logger)
logger.info(`Starting transitiong for user ${userUuid} ...`)
const triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser =
container.get(TYPES.Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser)
Promise.resolve(requestTransition(triggerTransitionFromPrimaryToSecondaryDatabaseForUser, logger))
.then(() => {
logger.info(`Transition triggered for user ${userUuid}`)
process.exit(0)
})
.catch((error) => {
logger.error(`Could not trigger transition for user ${userUuid}: ${error.message}`)
process.exit(1)
})
})
@@ -1,11 +0,0 @@
'use strict'
const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/transition.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index
@@ -14,12 +14,6 @@ case "$COMMAND" in
node docker/entrypoint-worker.js
;;
'transition' )
echo "[Docker] Starting transition Single User..."
USER_UUID=$1 && shift 1
node docker/entrypoint-transition.js $USER_UUID
;;
* )
echo "[Docker] Unknown command"
;;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.95.15",
"version": "1.96.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -1090,9 +1090,6 @@ export class ContainerConfigLoader {
container.get<SyncItems>(TYPES.Sync_SyncItems),
container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
container.get<GetItem>(TYPES.Sync_GetItem),
container.get<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
),
container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
@@ -172,7 +172,8 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: {
userUuid: string
transitionType: 'items' | 'revisions'
status: 'STARTED' | 'FAILED' | 'FINISHED'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent {
return {
type: 'TRANSITION_STATUS_UPDATED',
@@ -184,10 +185,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
},
origin: DomainEventService.SyncingServer,
},
payload: {
transitionTimestamp: this.timer.getTimestampInMicroseconds(),
...dto,
},
payload: dto,
}
}
@@ -52,7 +52,8 @@ export interface DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: {
userUuid: string
transitionType: 'items' | 'revisions'
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent
createEmailRequestedEvent(dto: {
userEmail: string
@@ -18,6 +18,7 @@ export class TransitionRequestedEventHandler implements DomainEventHandlerInterf
const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
userUuid: event.payload.userUuid,
transitionTimestamp: event.payload.timestamp,
})
if (result.isFailed()) {
@@ -18,13 +18,18 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
) {}
async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'items') {
if (event.payload.transitionType !== 'items') {
return
}
if (event.payload.status === 'STARTED') {
if (await this.isAlreadyMigrated(event.payload.userUuid)) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'FINISHED',
transitionType: 'items',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
@@ -36,6 +41,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid,
status: 'IN_PROGRESS',
transitionType: 'items',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
@@ -51,6 +57,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid,
status: 'FAILED',
transitionType: 'items',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
@@ -62,8 +69,18 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid,
status: 'FINISHED',
transitionType: 'items',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
} else if (event.payload.status === 'FINISHED') {
if (await this.isAlreadyMigrated(event.payload.userUuid)) {
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'VERIFIED',
transitionType: 'items',
transitionTimestamp: event.payload.transitionTimestamp,
})
}
}
}
@@ -298,7 +298,7 @@ describe('UpdateExistingItem', () => {
expect(itemRepository.save).toHaveBeenCalled()
})
it('should return error if created at time is not give in any form', async () => {
it('should fallback to updated at timestamp if created at time is not give in any form', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
@@ -308,13 +308,59 @@ describe('UpdateExistingItem', () => {
...itemHash1.props,
created_at: undefined,
created_at_timestamp: undefined,
updated_at_timestamp: 123,
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeTruthy()
expect(result.isFailed()).toBeFalsy()
expect(itemRepository.save).toHaveBeenCalled()
})
it('should fallback to updated at date if created at time is not give in any form', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
existingItem: item1,
onGoingRevisionsTransition: false,
itemHash: ItemHash.create({
...itemHash1.props,
created_at: undefined,
created_at_timestamp: undefined,
updated_at_timestamp: undefined,
updated_at: '2020-01-01T00:00:00.000Z',
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
expect(itemRepository.save).toHaveBeenCalled()
})
it('should fallback to 0 if created at and update at time is not give in any form', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
existingItem: item1,
onGoingRevisionsTransition: false,
itemHash: ItemHash.create({
...itemHash1.props,
created_at: undefined,
created_at_timestamp: undefined,
updated_at_timestamp: undefined,
updated_at: undefined,
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
expect(itemRepository.save).toHaveBeenCalled()
})
it('should return error if dates could not be created from timestamps', async () => {
@@ -23,6 +23,7 @@ import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOper
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
import { ItemHash } from '../../../Item/ItemHash'
export class UpdateExistingItem implements UseCaseInterface<Item> {
constructor(
@@ -115,17 +116,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
)
const updatedAtDate = this.timer.convertMicrosecondsToDate(updatedAtTimestamp)
let createdAtTimestamp: number
let createdAtDate: Date
if (dto.itemHash.props.created_at_timestamp) {
createdAtTimestamp = dto.itemHash.props.created_at_timestamp
createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
} else if (dto.itemHash.props.created_at) {
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.props.created_at)
createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.props.created_at)
} else {
return Result.fail('Created at timestamp is required.')
}
const { createdAtDate, createdAtTimestamp } = this.determineCreatedAt(dto.itemHash)
const datesOrError = Dates.create(createdAtDate, updatedAtDate)
if (datesOrError.isFailed()) {
@@ -221,6 +212,29 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
return Result.ok(dto.existingItem)
}
private determineCreatedAt(itemHash: ItemHash): { createdAtDate: Date; createdAtTimestamp: number } {
let createdAtTimestamp: number
let createdAtDate: Date
if (itemHash.props.created_at_timestamp) {
createdAtTimestamp = itemHash.props.created_at_timestamp
createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
} else if (itemHash.props.created_at) {
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(itemHash.props.created_at)
createdAtDate = this.timer.convertStringDateToDate(itemHash.props.created_at)
} else if (itemHash.props.updated_at_timestamp) {
createdAtTimestamp = itemHash.props.updated_at_timestamp
createdAtDate = this.timer.convertMicrosecondsToDate(itemHash.props.updated_at_timestamp)
} else if (itemHash.props.updated_at) {
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(itemHash.props.updated_at)
createdAtDate = this.timer.convertStringDateToDate(itemHash.props.updated_at)
} else {
createdAtTimestamp = 0
createdAtDate = new Date(0)
}
return { createdAtDate, createdAtTimestamp }
}
private async addNotificationsAndPublishEvents(
userUuid: Uuid,
sharedVaultOperation: SharedVaultOperationOnItem | null,
@@ -31,29 +31,41 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
const userUuid = userUuidOrError.getValue()
let newItemsInSecondaryCount = 0
let updatedItemsInSecondary: Item[] = []
if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
const newItems = await this.getNewItemsCreatedInSecondaryDatabase(userUuid)
for (const existingItem of newItems.alreadyExistingInPrimary) {
const { alreadyExistingInPrimary, newItemsInSecondary, updatedInSecondary } =
await this.getNewItemsCreatedInSecondaryDatabase(userUuid)
for (const existingItem of alreadyExistingInPrimary) {
this.logger.info(`Removing item ${existingItem.uuid.value} from secondary database`)
await (this.secondaryItemRepository as ItemRepositoryInterface).remove(existingItem)
}
if (newItems.newItemsInSecondary.length > 0) {
if (newItemsInSecondary.length > 0) {
this.logger.info(
`Found ${newItems.newItemsInSecondary.length} new items in secondary database for user ${userUuid.value}`,
`Found ${newItemsInSecondary.length} new items in secondary database for user ${userUuid.value}`,
)
}
newItemsInSecondaryCount = newItems.newItemsInSecondary.length
newItemsInSecondaryCount = newItemsInSecondary.length
if (updatedInSecondary.length > 0) {
this.logger.info(
`Found ${updatedInSecondary.length} updated items in secondary database for user ${userUuid.value}`,
)
}
updatedItemsInSecondary = updatedInSecondary
}
const updatedItemsInSecondaryCount = updatedItemsInSecondary.length
await this.allowForSecondaryDatabaseToCatchUp()
const migrationTimeStart = this.timer.getTimestampInMicroseconds()
const migrationResult = await this.migrateItemsForUser(userUuid)
const migrationResult = await this.migrateItemsForUser(userUuid, updatedItemsInSecondary)
if (migrationResult.isFailed()) {
if (newItemsInSecondaryCount === 0) {
if (newItemsInSecondaryCount === 0 && updatedItemsInSecondaryCount === 0) {
const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
@@ -70,9 +82,10 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid,
newItemsInSecondaryCount,
updatedItemsInSecondary,
)
if (integrityCheckResult.isFailed()) {
if (newItemsInSecondaryCount === 0) {
if (newItemsInSecondaryCount === 0 && updatedItemsInSecondaryCount === 0) {
const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
@@ -124,34 +137,46 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
private async getNewItemsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
alreadyExistingInPrimary: Item[]
newItemsInSecondary: Item[]
updatedInSecondary: Item[]
}> {
const items = await (this.secondaryItemRepository as ItemRepositoryInterface).findAll({
userUuid: userUuid.value,
})
const alreadyExistingInPrimary: Item[] = []
const updatedInSecondary: Item[] = []
const newItemsInSecondary: Item[] = []
for (const item of items) {
const itemExistsInPrimary = await this.checkIfItemExistsInPrimaryDatabase(item)
if (itemExistsInPrimary) {
const { itemInPrimary, newerItemInSecondary } = await this.checkIfItemExistsInPrimaryDatabase(item)
if (itemInPrimary !== null) {
alreadyExistingInPrimary.push(item)
} else {
continue
}
if (newerItemInSecondary !== null) {
updatedInSecondary.push(newerItemInSecondary)
continue
}
if (itemInPrimary === null && newerItemInSecondary === null) {
newItemsInSecondary.push(item)
continue
}
}
return {
alreadyExistingInPrimary: alreadyExistingInPrimary,
newItemsInSecondary: newItemsInSecondary,
alreadyExistingInPrimary,
newItemsInSecondary,
updatedInSecondary,
}
}
private async checkIfItemExistsInPrimaryDatabase(item: Item): Promise<boolean> {
private async checkIfItemExistsInPrimaryDatabase(
item: Item,
): Promise<{ itemInPrimary: Item | null; newerItemInSecondary: Item | null }> {
const itemInPrimary = await this.primaryItemRepository.findByUuid(item.uuid)
if (itemInPrimary === null) {
return false
return { itemInPrimary: null, newerItemInSecondary: null }
}
if (!item.isIdenticalTo(itemInPrimary)) {
@@ -161,13 +186,16 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
)}, revision in primary database: ${JSON.stringify(itemInPrimary)}`,
)
return false
return {
itemInPrimary: null,
newerItemInSecondary: item.props.timestamps.updatedAt > itemInPrimary.props.timestamps.updatedAt ? item : null,
}
}
return true
return { itemInPrimary: itemInPrimary, newerItemInSecondary: null }
}
private async migrateItemsForUser(userUuid: Uuid): Promise<Result<void>> {
private async migrateItemsForUser(userUuid: Uuid, updatedItemsInSecondary: Item[]): Promise<Result<void>> {
try {
const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
const totalPages = Math.ceil(totalItemsCountForUser / this.pageSize)
@@ -181,6 +209,11 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
const items = await this.primaryItemRepository.findAll(query)
for (const item of items) {
if (updatedItemsInSecondary.find((updatedItem) => updatedItem.uuid.equals(item.uuid))) {
this.logger.info(`Skipping saving item ${item.uuid.value} as it was updated in secondary database`)
continue
}
await (this.secondaryItemRepository as ItemRepositoryInterface).save(item)
}
}
@@ -204,6 +237,7 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid: Uuid,
newItemsInSecondaryCount: number,
updatedItemsInSecondary: Item[],
): Promise<Result<boolean>> {
try {
const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
@@ -235,6 +269,13 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
return Result.fail(`Item ${item.uuid.value} not found in secondary database`)
}
if (updatedItemsInSecondary.find((updatedItem) => updatedItem.uuid.equals(item.uuid))) {
this.logger.info(
`Skipping integrity check for item ${item.uuid.value} as it was updated in secondary database`,
)
continue
}
if (!item.isIdenticalTo(itemInSecondary)) {
return Result.fail(
`Item ${
@@ -23,6 +23,7 @@ describe('TriggerTransitionFromPrimaryToSecondaryDatabaseForUser', () => {
await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
transitionTimestamp: 123,
})
expect(domainEventPubliser.publish).toHaveBeenCalled()
@@ -15,6 +15,7 @@ export class TriggerTransitionFromPrimaryToSecondaryDatabaseForUser implements U
userUuid: dto.userUuid,
status: 'STARTED',
transitionType: 'items',
transitionTimestamp: dto.transitionTimestamp,
})
await this.domainEventPubliser.publish(event)
@@ -1,3 +1,4 @@
export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO {
userUuid: string
transitionTimestamp: number
}
@@ -11,7 +11,6 @@ import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { BaseItemsController } from './Base/BaseItemsController'
import { MapperInterface } from '@standardnotes/domain-core'
import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
@controller('/items', TYPES.Sync_AuthMiddleware)
export class AnnotatedItemsController extends BaseItemsController {
@@ -19,20 +18,11 @@ export class AnnotatedItemsController extends BaseItemsController {
@inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
@inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
@inject(TYPES.Sync_GetItem) override getItem: GetItem,
@inject(TYPES.Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser)
override triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
@inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
@inject(TYPES.Sync_SyncResponseFactoryResolver)
override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
) {
super(
syncItems,
checkIntegrity,
getItem,
triggerTransitionFromPrimaryToSecondaryDatabaseForUser,
itemHttpMapper,
syncResponseFactoryResolver,
)
super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver)
}
@httpPost('/sync')
@@ -45,11 +35,6 @@ export class AnnotatedItemsController extends BaseItemsController {
return super.checkItemsIntegrity(request, response)
}
@httpPost('/transition')
override async transition(request: Request, response: Response): Promise<results.JsonResult> {
return super.transition(request, response)
}
@httpGet('/:uuid')
override async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
return super.getSingleItem(request, response)
@@ -12,14 +12,12 @@ import { ApiVersion } from '../../../Domain/Api/ApiVersion'
import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
import { ItemHash } from '../../../Domain/Item/ItemHash'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
export class BaseItemsController extends BaseHttpController {
constructor(
protected syncItems: SyncItems,
protected checkIntegrity: CheckIntegrity,
protected getItem: GetItem,
protected triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
private controllerContainer?: ControllerContainerInterface,
@@ -30,7 +28,6 @@ export class BaseItemsController extends BaseHttpController {
this.controllerContainer.register('sync.items.sync', this.sync.bind(this))
this.controllerContainer.register('sync.items.check_integrity', this.checkItemsIntegrity.bind(this))
this.controllerContainer.register('sync.items.get_item', this.getSingleItem.bind(this))
this.controllerContainer.register('sync.items.transition', this.transition.bind(this))
}
}
@@ -113,25 +110,6 @@ export class BaseItemsController extends BaseHttpController {
})
}
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 })
}
async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.getItem.execute({
userUuid: response.locals.user.uuid,
@@ -18,7 +18,6 @@ export class MongoDBItem {
declare content: string | null
@Column()
@Index('index_items_on_content_type')
declare contentType: string | null
@Column()
+4
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.42](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.41...@standardnotes/websockets-server@1.10.42) (2023-09-15)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.10.41](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.40...@standardnotes/websockets-server@1.10.41) (2023-09-13)
**Note:** Version bump only for package @standardnotes/websockets-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.10.41",
"version": "1.10.42",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+52 -75
View File
@@ -2659,10 +2659,10 @@ __metadata:
languageName: node
linkType: hard
"@hexagon/base64@npm:^1.1.25":
version: 1.1.26
resolution: "@hexagon/base64@npm:1.1.26"
checksum: e42582ed12465bffaf96307c9d5b7dfd36166ec4dc41a1838b9a560c90c9d136d006099a205e1684fb0dc18002cc5af51a49dd7b81a7c4b86798372d6ee26af3
"@hexagon/base64@npm:^1.1.27":
version: 1.1.27
resolution: "@hexagon/base64@npm:1.1.27"
checksum: 899fffaf54b291e1df997bf33dbf6e068fcfbd83155adc114e14bcb9c1e36c5f820dfaaee3d0c2409f7e84efa4352f51655eac8bec4c2432fca443bf179bce8d
languageName: node
linkType: hard
@@ -3793,7 +3793,7 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-android@npm:^2.3.3":
"@peculiar/asn1-android@npm:^2.3.6":
version: 2.3.6
resolution: "@peculiar/asn1-android@npm:2.3.6"
dependencies:
@@ -3804,7 +3804,7 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-ecc@npm:^2.3.4":
"@peculiar/asn1-ecc@npm:^2.3.6":
version: 2.3.6
resolution: "@peculiar/asn1-ecc@npm:2.3.6"
dependencies:
@@ -3816,7 +3816,7 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-rsa@npm:^2.3.4":
"@peculiar/asn1-rsa@npm:^2.3.6":
version: 2.3.6
resolution: "@peculiar/asn1-rsa@npm:2.3.6"
dependencies:
@@ -3828,7 +3828,7 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-schema@npm:^2.3.3, @peculiar/asn1-schema@npm:^2.3.6":
"@peculiar/asn1-schema@npm:^2.3.6":
version: 2.3.6
resolution: "@peculiar/asn1-schema@npm:2.3.6"
dependencies:
@@ -3839,7 +3839,7 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-x509@npm:^2.3.4, @peculiar/asn1-x509@npm:^2.3.6":
"@peculiar/asn1-x509@npm:^2.3.6":
version: 2.3.6
resolution: "@peculiar/asn1-x509@npm:2.3.6"
dependencies:
@@ -3987,41 +3987,27 @@ __metadata:
languageName: node
linkType: hard
"@simplewebauthn/iso-webcrypto@npm:^7.2.0":
version: 7.2.0
resolution: "@simplewebauthn/iso-webcrypto@npm:7.2.0"
"@simplewebauthn/server@npm:^8.1.1":
version: 8.1.1
resolution: "@simplewebauthn/server@npm:8.1.1"
dependencies:
"@simplewebauthn/typescript-types": "npm:*"
"@types/node": "npm:^18.11.9"
checksum: b57899d0ada391507ce8f4601328ed62df5d09f75f6e91b018278631270a96f37ceab95f9e824c9555dd05820e5a99ac386ed067db7902d37bdb6d995fbd7eaf
"@hexagon/base64": "npm:^1.1.27"
"@peculiar/asn1-android": "npm:^2.3.6"
"@peculiar/asn1-ecc": "npm:^2.3.6"
"@peculiar/asn1-rsa": "npm:^2.3.6"
"@peculiar/asn1-schema": "npm:^2.3.6"
"@peculiar/asn1-x509": "npm:^2.3.6"
"@simplewebauthn/typescript-types": "npm:^8.0.0"
cbor-x: "npm:^1.5.2"
cross-fetch: "npm:^4.0.0"
checksum: a07c2a067b25b7f4afe215dcf81e0280aa5c382f5eb3cb1ef9327c6dc84c9618ed5774fc61bbc5717b8633dd5b8b4c41c7800cb32e83c6a4b1d2d1e1e50f5250
languageName: node
linkType: hard
"@simplewebauthn/server@npm:^7.2.0":
version: 7.2.0
resolution: "@simplewebauthn/server@npm:7.2.0"
dependencies:
"@hexagon/base64": "npm:^1.1.25"
"@peculiar/asn1-android": "npm:^2.3.3"
"@peculiar/asn1-ecc": "npm:^2.3.4"
"@peculiar/asn1-rsa": "npm:^2.3.4"
"@peculiar/asn1-schema": "npm:^2.3.3"
"@peculiar/asn1-x509": "npm:^2.3.4"
"@simplewebauthn/iso-webcrypto": "npm:^7.2.0"
"@simplewebauthn/typescript-types": "npm:*"
"@types/debug": "npm:^4.1.7"
"@types/node": "npm:^18.11.9"
cbor-x: "npm:^1.4.1"
cross-fetch: "npm:^3.1.5"
debug: "npm:^4.3.2"
checksum: 2e37c87edd05abace8ba8c5b1f4e2cb4adb9ec4dcf0b237d25f375f35538d25e31cc0ae196029006cf5124c983216a3cf69127732942863d2960bd72ed5783c4
languageName: node
linkType: hard
"@simplewebauthn/typescript-types@npm:*, @simplewebauthn/typescript-types@npm:^7.0.0":
version: 7.0.0
resolution: "@simplewebauthn/typescript-types@npm:7.0.0"
checksum: 124238ea1859c80761c4cdbf19107e2e8e96fdefa64affb55fb4fc67d1ac5e3354c3098c908729d2de439a633115d98da77ded7289286fe576559306fa933815
"@simplewebauthn/typescript-types@npm:^8.0.0":
version: 8.0.0
resolution: "@simplewebauthn/typescript-types@npm:8.0.0"
checksum: 21e0b13268f237d7cd6ecdc6cdceb884ddcc85e18a3554b65d10da4f502056aefa810db94f504beaad96b4ac0c7861a022954dcc6aa8721d4215571c2c3dcdf5
languageName: node
linkType: hard
@@ -4744,8 +4730,8 @@ __metadata:
"@cbor-extract/cbor-extract-linux-arm64": "npm:^2.1.1"
"@cbor-extract/cbor-extract-linux-x64": "npm:^2.1.1"
"@newrelic/winston-enricher": "npm:^4.0.1"
"@simplewebauthn/server": "npm:^7.2.0"
"@simplewebauthn/typescript-types": "npm:^7.0.0"
"@simplewebauthn/server": "npm:^8.1.1"
"@simplewebauthn/typescript-types": "npm:^8.0.0"
"@standardnotes/api": "npm:^1.26.26"
"@standardnotes/common": "workspace:*"
"@standardnotes/domain-core": "workspace:^"
@@ -5589,15 +5575,6 @@ __metadata:
languageName: node
linkType: hard
"@types/debug@npm:^4.1.7":
version: 4.1.8
resolution: "@types/debug@npm:4.1.8"
dependencies:
"@types/ms": "npm:*"
checksum: 9c190e812984e0f6e02dfdfb0c7a3081a55cf3fc712a4e059336bd9f8329db70211eb851ce409311520876549cff2c4785ce48dd4c9fef8e48549c87bec29ded
languageName: node
linkType: hard
"@types/dotenv@npm:^8.2.0":
version: 8.2.0
resolution: "@types/dotenv@npm:8.2.0"
@@ -5792,13 +5769,6 @@ __metadata:
languageName: node
linkType: hard
"@types/ms@npm:*":
version: 0.7.31
resolution: "@types/ms@npm:0.7.31"
checksum: cccb52777bb683c65ac5bab61351cd3910c9ce3512b1d903a591fc9694bb83afad6e48bf0beee5b47b6a8b620a05f5d82f8febfd55de05e7d9eb93586cc196c8
languageName: node
linkType: hard
"@types/newrelic@npm:^9.14.0":
version: 9.14.0
resolution: "@types/newrelic@npm:9.14.0"
@@ -5820,13 +5790,6 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^18.11.9":
version: 18.16.16
resolution: "@types/node@npm:18.16.16"
checksum: 946bd4d8e6fa54220e4193bc594de8a2e138e6afebb6efb7d862d98e30ced25a19476a6f47c81e690b9ac77f616f64217e0bcf4811916ccd9b5935e5bea0e4a0
languageName: node
linkType: hard
"@types/node@npm:^20.5.7":
version: 20.5.7
resolution: "@types/node@npm:20.5.7"
@@ -7208,15 +7171,15 @@ __metadata:
languageName: node
linkType: hard
"cbor-x@npm:^1.4.1":
version: 1.5.3
resolution: "cbor-x@npm:1.5.3"
"cbor-x@npm:^1.5.2":
version: 1.5.4
resolution: "cbor-x@npm:1.5.4"
dependencies:
cbor-extract: "npm:^2.1.1"
dependenciesMeta:
cbor-extract:
optional: true
checksum: d4df85b33969826f4c96a4b4a8fbe03132fb0817fba876f16d41ad6d1a7d2668ec04c923f313220506029cc2b5ab212901ba24b4594d0115e0f527ef31506fbf
checksum: 742aea498abfe004a7ff4db2a1c0e00d9e9c1d89db4ad9aa94a9b886cd2ce10a133f20e32788c83696eef368e18c2b5bc82e4b1480c5af91937816a5630989d6
languageName: node
linkType: hard
@@ -7848,12 +7811,12 @@ __metadata:
languageName: node
linkType: hard
"cross-fetch@npm:^3.1.5":
version: 3.1.6
resolution: "cross-fetch@npm:3.1.6"
"cross-fetch@npm:^4.0.0":
version: 4.0.0
resolution: "cross-fetch@npm:4.0.0"
dependencies:
node-fetch: "npm:^2.6.11"
checksum: a8989fca821cae97520976d00f85ce7c3ab8af7e00cc06c94fd94c49ada6847f4cdeabca8e0ebd4aa6c7343f70bea7e0c64d5910b846aab218136a450585aa61
node-fetch: "npm:^2.6.12"
checksum: 30e86b703a455baca17b7f2088fdd88b71193b39e7cb61f3385511dc6064b7741c816329c0abff8a74d306969455c8797131d056518a981fd4d2424ecd4ab451
languageName: node
linkType: hard
@@ -12007,7 +11970,21 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7":
"node-fetch@npm:^2.6.12":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: a3ad7889038bf6c49046272515d4f0e3167088b40fd37e1cc6eeea745f5a68cec798d55ac3210e2bc51891cb745e3dc30a734cc5f4b4df764f45886881b198b1
languageName: node
linkType: hard
"node-fetch@npm:^2.6.7":
version: 2.6.11
resolution: "node-fetch@npm:2.6.11"
dependencies: