Compare commits

...

38 Commits

Author SHA1 Message Date
standardci ac0390e7c3 chore(release): publish new version
- @standardnotes/auth-server@1.142.1
 - @standardnotes/home-server@1.15.57
 - @standardnotes/revisions-server@1.34.1
 - @standardnotes/syncing-server@1.96.1
2023-09-15 11:15:32 +00:00
Karol Sójko 0477507a6a fix: add debug logs for updating transition status on auth 2023-09-15 12:57:43 +02:00
Karol Sójko 3e7856c895 fix: add debug logs for transition status updates 2023-09-15 12:55:04 +02:00
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
standardci 179d8eaaa1 chore(release): publish new version
- @standardnotes/analytics@2.26.12
 - @standardnotes/api-gateway@1.74.9
 - @standardnotes/auth-server@1.141.9
 - @standardnotes/domain-events-infra@1.12.28
 - @standardnotes/domain-events@2.125.4
 - @standardnotes/event-store@1.11.40
 - @standardnotes/files-server@1.22.19
 - @standardnotes/home-server@1.15.49
 - @standardnotes/revisions-server@1.33.18
 - @standardnotes/scheduler-server@1.20.44
 - @standardnotes/syncing-server@1.95.15
 - @standardnotes/websockets-server@1.10.41
2023-09-13 07:47:36 +00:00
Karol Sójko 38685c1861 fix: display transition progress in logs 2023-09-13 09:31:07 +02:00
standardci cdf42fbe2d chore(release): publish new version
- @standardnotes/home-server@1.15.48
 - @standardnotes/revisions-server@1.33.17
 - @standardnotes/syncing-server@1.95.14
2023-09-13 06:39:14 +00:00
Karol Sójko 9be4c002b7 fix: setting status for already migrated users 2023-09-13 08:21:21 +02:00
standardci a16c5307a0 chore(release): publish new version
- @standardnotes/home-server@1.15.47
 - @standardnotes/revisions-server@1.33.16
 - @standardnotes/syncing-server@1.95.13
2023-09-13 04:50:15 +00:00
Karol Sójko d5536f5430 fix(syncing-server): case insensitive integrity check 2023-09-13 06:33:31 +02:00
Karol Sójko b1d88b15be fix: cleanup only for 0 new items 2023-09-12 22:51:39 +02:00
Karol Sójko ff78285e43 fix(syncing-server): add catch up timeout for secondary db 2023-09-12 22:24:48 +02:00
standardci 1a26221385 chore(release): publish new version
- @standardnotes/auth-server@1.141.8
 - @standardnotes/home-server@1.15.46
 - @standardnotes/revisions-server@1.33.15
 - @standardnotes/syncing-server@1.95.12
2023-09-12 20:05:02 +00:00
Karol Sójko 54113abe2a fix: imports 2023-09-12 21:31:58 +02:00
Karol Sójko afe385aed4 fix(auth): remove the transition role constraint 2023-09-12 21:28:48 +02:00
Karol Sójko f055e52e06 fix(auth): add transition role only if the items transition has completed 2023-09-12 21:26:40 +02:00
Karol Sójko fab5d18064 fix: sync between primary and secondary database with diff 2023-09-12 21:00:01 +02:00
standardci a1e654a0d0 chore(release): publish new version
- @standardnotes/home-server@1.15.45
 - @standardnotes/revisions-server@1.33.14
2023-09-12 16:21:14 +00:00
Karol Sójko aa835268ea fix(revisions): handle transitions with already existing data in secondary 2023-09-12 17:42:11 +02:00
standardci 74b4312928 chore(release): publish new version
- @standardnotes/home-server@1.15.44
 - @standardnotes/syncing-server@1.95.11
2023-09-12 15:17:12 +00:00
Karol Sójko e91a832152 fix(syncing-server): binding 2023-09-12 16:41:39 +02:00
96 changed files with 1058 additions and 1592 deletions
Generated
+43 -63
View File
@@ -3326,10 +3326,10 @@ const RAW_RUNTIME_STATE =
}]\ }]\
]],\ ]],\
["@hexagon/base64", [\ ["@hexagon/base64", [\
["npm:1.1.26", {\ ["npm:1.1.27", {\
"packageLocation": "./.yarn/cache/@hexagon-base64-npm-1.1.26-dbfda05df8-e42582ed12.zip/node_modules/@hexagon/base64/",\ "packageLocation": "./.yarn/cache/@hexagon-base64-npm-1.1.27-df6f264962-899fffaf54.zip/node_modules/@hexagon/base64/",\
"packageDependencies": [\ "packageDependencies": [\
["@hexagon/base64", "npm:1.1.26"]\ ["@hexagon/base64", "npm:1.1.27"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
@@ -5067,44 +5067,29 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "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", [\ ["@simplewebauthn/server", [\
["npm:7.2.0", {\ ["npm:8.1.1", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-7.2.0-f1ed5fde8a-2e37c87edd.zip/node_modules/@simplewebauthn/server/",\ "packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-8.1.1-106d3bd108-a07c2a067b.zip/node_modules/@simplewebauthn/server/",\
"packageDependencies": [\ "packageDependencies": [\
["@simplewebauthn/server", "npm:7.2.0"],\ ["@simplewebauthn/server", "npm:8.1.1"],\
["@hexagon/base64", "npm:1.1.26"],\ ["@hexagon/base64", "npm:1.1.27"],\
["@peculiar/asn1-android", "npm:2.3.6"],\ ["@peculiar/asn1-android", "npm:2.3.6"],\
["@peculiar/asn1-ecc", "npm:2.3.6"],\ ["@peculiar/asn1-ecc", "npm:2.3.6"],\
["@peculiar/asn1-rsa", "npm:2.3.6"],\ ["@peculiar/asn1-rsa", "npm:2.3.6"],\
["@peculiar/asn1-schema", "npm:2.3.6"],\ ["@peculiar/asn1-schema", "npm:2.3.6"],\
["@peculiar/asn1-x509", "npm:2.3.6"],\ ["@peculiar/asn1-x509", "npm:2.3.6"],\
["@simplewebauthn/iso-webcrypto", "npm:7.2.0"],\ ["@simplewebauthn/typescript-types", "npm:8.0.0"],\
["@simplewebauthn/typescript-types", "npm:7.0.0"],\ ["cbor-x", "npm:1.5.4"],\
["@types/debug", "npm:4.1.8"],\ ["cross-fetch", "npm:4.0.0"]\
["@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"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@simplewebauthn/typescript-types", [\ ["@simplewebauthn/typescript-types", [\
["npm:7.0.0", {\ ["npm:8.0.0", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-7.0.0-cc6ca20415-124238ea18.zip/node_modules/@simplewebauthn/typescript-types/",\ "packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-8.0.0-f3b313c27b-21e0b13268.zip/node_modules/@simplewebauthn/typescript-types/",\
"packageDependencies": [\ "packageDependencies": [\
["@simplewebauthn/typescript-types", "npm:7.0.0"]\ ["@simplewebauthn/typescript-types", "npm:8.0.0"]\
],\ ],\
"linkType": "HARD"\ "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-arm64", "npm:2.1.1"],\
["@cbor-extract/cbor-extract-linux-x64", "npm:2.1.1"],\ ["@cbor-extract/cbor-extract-linux-x64", "npm:2.1.1"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
["@simplewebauthn/server", "npm:7.2.0"],\ ["@simplewebauthn/server", "npm:8.1.1"],\
["@simplewebauthn/typescript-types", "npm:7.0.0"],\ ["@simplewebauthn/typescript-types", "npm:8.0.0"],\
["@standardnotes/api", "npm:1.26.26"],\ ["@standardnotes/api", "npm:1.26.26"],\
["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -6720,16 +6705,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "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", [\ ["@types/dotenv", [\
["npm:8.2.0", {\ ["npm:8.2.0", {\
"packageLocation": "./.yarn/cache/@types-dotenv-npm-8.2.0-f4d0e3d65b-13f90a36f7.zip/node_modules/@types/dotenv/",\ "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"\ "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", [\ ["@types/newrelic", [\
["npm:9.14.0", {\ ["npm:9.14.0", {\
"packageLocation": "./.yarn/cache/@types-newrelic-npm-9.14.0-4668da51a1-2ec951bd8f.zip/node_modules/@types/newrelic/",\ "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"\ "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", {\ ["npm:20.2.5", {\
"packageLocation": "./.yarn/cache/@types-node-npm-20.2.5-0014d2d9ce-55e4f8d08e.zip/node_modules/@types/node/",\ "packageLocation": "./.yarn/cache/@types-node-npm-20.2.5-0014d2d9ce-55e4f8d08e.zip/node_modules/@types/node/",\
"packageDependencies": [\ "packageDependencies": [\
@@ -8706,10 +8665,10 @@ const RAW_RUNTIME_STATE =
}]\ }]\
]],\ ]],\
["cbor-x", [\ ["cbor-x", [\
["npm:1.5.3", {\ ["npm:1.5.4", {\
"packageLocation": "./.yarn/cache/cbor-x-npm-1.5.3-1d452dd267-d4df85b339.zip/node_modules/cbor-x/",\ "packageLocation": "./.yarn/cache/cbor-x-npm-1.5.4-2d5a649a4b-742aea498a.zip/node_modules/cbor-x/",\
"packageDependencies": [\ "packageDependencies": [\
["cbor-x", "npm:1.5.3"],\ ["cbor-x", "npm:1.5.4"],\
["cbor-extract", "npm:2.1.1"]\ ["cbor-extract", "npm:2.1.1"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
@@ -9434,11 +9393,11 @@ const RAW_RUNTIME_STATE =
}]\ }]\
]],\ ]],\
["cross-fetch", [\ ["cross-fetch", [\
["npm:3.1.6", {\ ["npm:4.0.0", {\
"packageLocation": "./.yarn/cache/cross-fetch-npm-3.1.6-cdb982d446-a8989fca82.zip/node_modules/cross-fetch/",\ "packageLocation": "./.yarn/cache/cross-fetch-npm-4.0.0-9c67668db4-30e86b703a.zip/node_modules/cross-fetch/",\
"packageDependencies": [\ "packageDependencies": [\
["cross-fetch", "npm:3.1.6"],\ ["cross-fetch", "npm:4.0.0"],\
["node-fetch", "virtual:0f92dfe7f9dc4fd492639d4a5b7805c2b27442bf599fd4f370b22a7966ba078f5d4525e2a8e8af29369f20e1833ed084bd52be59679efaa6c1c6c10cdbcd8baa#npm:2.6.11"]\ ["node-fetch", "virtual:9c67668db478e95ba4d6a763bc55027eeff0d22eaf59478017ea07386fc33a3c7b7b625af78aa86a33991a9a500a7aa216e28632de568f02adefd662ef53a42d#npm:2.7.0"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
@@ -14174,6 +14133,13 @@ const RAW_RUNTIME_STATE =
],\ ],\
"linkType": "SOFT"\ "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", {\ ["npm:3.3.1", {\
"packageLocation": "./.yarn/cache/node-fetch-npm-3.3.1-576511fc5a-1d0c635bdf.zip/node_modules/node-fetch/",\ "packageLocation": "./.yarn/cache/node-fetch-npm-3.3.1-576511fc5a-1d0c635bdf.zip/node_modules/node-fetch/",\
"packageDependencies": [\ "packageDependencies": [\
@@ -14197,6 +14163,20 @@ const RAW_RUNTIME_STATE =
"encoding"\ "encoding"\
],\ ],\
"linkType": "HARD"\ "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", [\ ["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.
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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
## [2.26.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.10...@standardnotes/analytics@2.26.11) (2023-09-12) ## [2.26.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.10...@standardnotes/analytics@2.26.11) (2023-09-12)
**Note:** Version bump only for package @standardnotes/analytics **Note:** Version bump only for package @standardnotes/analytics
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/analytics", "name": "@standardnotes/analytics",
"version": "2.26.11", "version": "2.26.13",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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.74.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.7...@standardnotes/api-gateway@1.74.8) (2023-09-12) ## [1.74.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.7...@standardnotes/api-gateway@1.74.8) (2023-09-12)
**Note:** Version bump only for package @standardnotes/api-gateway **Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/api-gateway", "name": "@standardnotes/api-gateway",
"version": "1.74.8", "version": "1.74.11",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "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) @httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> { async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer( await this.httpService.callAuthServer(
@@ -43,7 +43,6 @@ export class EndpointResolver implements EndpointResolverInterface {
['[PATCH]:users/:userId', 'auth.users.update'], ['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'], ['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
['[GET]:users/params', 'auth.users.getKeyParams'], ['[GET]:users/params', 'auth.users.getKeyParams'],
['[GET]:users/transition-status', 'auth.users.transition-status'],
['[DELETE]:users/:userUuid', 'auth.users.delete'], ['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'], ['[POST]:listed', 'auth.users.createListedAccount'],
['[POST]:auth', 'auth.users.register'], ['[POST]:auth', 'auth.users.register'],
@@ -59,13 +58,11 @@ export class EndpointResolver implements EndpointResolverInterface {
// Syncing Server // Syncing Server
['[POST]:items/sync', 'sync.items.sync'], ['[POST]:items/sync', 'sync.items.sync'],
['[POST]:items/check-integrity', 'sync.items.check_integrity'], ['[POST]:items/check-integrity', 'sync.items.check_integrity'],
['[POST]:items/transition', 'sync.items.transition'],
['[GET]:items/:uuid', 'sync.items.get_item'], ['[GET]:items/:uuid', 'sync.items.get_item'],
// Revisions Controller V2 // Revisions Controller V2
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'], ['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],
['[GET]:items/:itemUuid/revisions/:id', 'revisions.revisions.getRevision'], ['[GET]:items/:itemUuid/revisions/:id', 'revisions.revisions.getRevision'],
['[DELETE]:items/:itemUuid/revisions/:id', 'revisions.revisions.deleteRevision'], ['[DELETE]:items/:itemUuid/revisions/:id', 'revisions.revisions.deleteRevision'],
['[POST]:revisions/transition', 'revisions.revisions.transition'],
// Messages Controller // Messages Controller
['[GET]:messages/', 'sync.messages.get-received'], ['[GET]:messages/', 'sync.messages.get-received'],
['[GET]:messages/outbound', 'sync.messages.get-sent'], ['[GET]:messages/outbound', 'sync.messages.get-sent'],
+57
View File
@@ -3,6 +3,63 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.142.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.142.0...@standardnotes/auth-server@1.142.1) (2023-09-15)
### Bug Fixes
* add debug logs for updating transition status on auth ([0477507](https://github.com/standardnotes/server/commit/0477507a6a951dc2bb904711c870e2c40a0664bd))
# [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
* display transition progress in logs ([38685c1](https://github.com/standardnotes/server/commit/38685c1861b13e398dd96aa39f2cf1aece2090fb))
## [1.141.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.7...@standardnotes/auth-server@1.141.8) (2023-09-12)
### Bug Fixes
* **auth:** add transition role only if the items transition has completed ([f055e52](https://github.com/standardnotes/server/commit/f055e52e06b6e93501abd340dfce214d5363bc30))
* **auth:** remove the transition role constraint ([afe385a](https://github.com/standardnotes/server/commit/afe385aed4ba5ca53d8ef429ae4154f4ccf81419))
* imports ([54113ab](https://github.com/standardnotes/server/commit/54113abe2a961720a3561e5ff3a0069046ea8d25))
## [1.141.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.6...@standardnotes/auth-server@1.141.7) (2023-09-12) ## [1.141.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.6...@standardnotes/auth-server@1.141.7) (2023-09-12)
### Bug Fixes ### Bug Fixes
+31 -23
View File
@@ -10,38 +10,52 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface' import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { RoleName } from '@standardnotes/domain-core' import { TimerInterface } from '@standardnotes/time'
import { TransitionStatusRepositoryInterface } from '../src/Domain/Transition/TransitionStatusRepositoryInterface' import { TransitionStatusRepositoryInterface } from '../src/Domain/Transition/TransitionStatusRepositoryInterface'
import { RoleName } from '@standardnotes/domain-core'
const inputArgs = process.argv.slice(2) const inputArgs = process.argv.slice(2)
const startDateString = inputArgs[0] const startDateString = inputArgs[0]
const endDateString = inputArgs[1] const endDateString = inputArgs[1]
const requestTransition = async ( const requestTransition = async (
userRepository: UserRepositoryInterface,
transitionStatusRepository: TransitionStatusRepositoryInterface, transitionStatusRepository: TransitionStatusRepositoryInterface,
userRepository: UserRepositoryInterface,
logger: Logger, logger: Logger,
domainEventFactory: DomainEventFactoryInterface, domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface, domainEventPublisher: DomainEventPublisherInterface,
timer: TimerInterface,
): Promise<void> => { ): Promise<void> => {
const startDate = new Date(startDateString) const startDate = new Date(startDateString)
const endDate = new Date(endDateString) const endDate = new Date(endDateString)
const users = await userRepository.findAllCreatedBetween(startDate, endDate) 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 let usersTriggered = 0
for (const user of users) { for (const user of users) {
const roles = await user.roles const itemsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'items')
const userHasTransitionUserRole = roles.some((role) => role.name === RoleName.NAMES.TransitionUser) === true const revisionsTransitionStatus = await transitionStatusRepository.getStatus(user.uuid, 'revisions')
if (userHasTransitionUserRole === true) {
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 continue
} }
const transitionRequestedEvent = domainEventFactory.createTransitionRequestedEvent({ const transitionRequestedEvent = domainEventFactory.createTransitionRequestedEvent({
userUuid: user.uuid, userUuid: user.uuid,
type: 'items', type: 'items',
timestamp,
}) })
usersTriggered += 1 usersTriggered += 1
@@ -50,22 +64,8 @@ const requestTransition = async (
} }
logger.info( 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') const container = new ContainerConfigLoader('worker')
@@ -82,12 +82,20 @@ void container.load().then((container) => {
const userRepository: UserRepositoryInterface = container.get(TYPES.Auth_UserRepository) const userRepository: UserRepositoryInterface = container.get(TYPES.Auth_UserRepository)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory) const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher) 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, TYPES.Auth_TransitionStatusRepository,
) )
Promise.resolve( Promise.resolve(
requestTransition(userRepository, transitionStatusRepository, logger, domainEventFactory, domainEventPublisher), requestTransition(
transitionStatusRepository,
userRepository,
logger,
domainEventFactory,
domainEventPublisher,
timer,
),
) )
.then(() => { .then(() => {
logger.info(`Finished transition request for users created between ${startDateString} and ${endDateString}`) logger.info(`Finished transition request for users created between ${startDateString} and ${endDateString}`)
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/auth-server", "name": "@standardnotes/auth-server",
"version": "1.141.7", "version": "1.142.1",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
@@ -40,8 +40,8 @@
"@aws-sdk/client-sqs": "^3.332.0", "@aws-sdk/client-sqs": "^3.332.0",
"@cbor-extract/cbor-extract-linux-arm64": "^2.1.1", "@cbor-extract/cbor-extract-linux-arm64": "^2.1.1",
"@cbor-extract/cbor-extract-linux-x64": "^2.1.1", "@cbor-extract/cbor-extract-linux-x64": "^2.1.1",
"@simplewebauthn/server": "^7.2.0", "@simplewebauthn/server": "^8.1.1",
"@simplewebauthn/typescript-types": "^7.0.0", "@simplewebauthn/typescript-types": "^8.0.0",
"@standardnotes/api": "^1.26.26", "@standardnotes/api": "^1.26.26",
"@standardnotes/common": "workspace:*", "@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-core": "workspace:^",
+1 -10
View File
@@ -263,7 +263,6 @@ import { RedisTransitionStatusRepository } from '../Infra/Redis/RedisTransitionS
import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository' import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTransitionStatusRepository'
import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler' import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus' import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser' import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
import { SharedVaultUserPersistenceMapper } from '../Mapping/SharedVaultUserPersistenceMapper' import { SharedVaultUserPersistenceMapper } from '../Mapping/SharedVaultUserPersistenceMapper'
import { SharedVaultUserRepositoryInterface } from '../Domain/SharedVault/SharedVaultUserRepositoryInterface' import { SharedVaultUserRepositoryInterface } from '../Domain/SharedVault/SharedVaultUserRepositoryInterface'
@@ -943,14 +942,7 @@ export class ContainerConfigLoader {
new UpdateTransitionStatus( new UpdateTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository), container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService), 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 container
@@ -1275,7 +1267,6 @@ export class ContainerConfigLoader {
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts), container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts), container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials), container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
container.get<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer), container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
), ),
) )
-1
View File
@@ -159,7 +159,6 @@ const TYPES = {
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'), Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'), Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'), Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'), Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'), Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'),
// Handlers // Handlers
@@ -33,7 +33,11 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface { export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {} 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 { return {
type: 'TRANSITION_REQUESTED', type: 'TRANSITION_REQUESTED',
createdAt: this.timer.getUTCDate(), createdAt: this.timer.getUTCDate(),
@@ -90,5 +90,9 @@ export interface DomainEventFactoryInterface {
}): StatisticPersistenceRequestedEvent }): StatisticPersistenceRequestedEvent
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent
createSessionRefreshedEvent(dto: { userUuid: string }): SessionRefreshedEvent createSessionRefreshedEvent(dto: { userUuid: string }): SessionRefreshedEvent
createTransitionRequestedEvent(dto: { userUuid: string; type: 'items' | 'revisions' }): TransitionRequestedEvent createTransitionRequestedEvent(dto: {
userUuid: string
type: 'items' | 'revisions'
timestamp: number
}): TransitionRequestedEvent
} }
@@ -13,6 +13,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
status: event.payload.status, status: event.payload.status,
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
transitionType: event.payload.transitionType, transitionType: event.payload.transitionType,
transitionTimestamp: event.payload.transitionTimestamp,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -2,14 +2,10 @@ export interface TransitionStatusRepositoryInterface {
updateStatus( updateStatus(
userUuid: string, userUuid: string,
transitionType: 'items' | 'revisions', transitionType: 'items' | 'revisions',
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED', status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED',
): Promise<void> ): Promise<void>
removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void>
getStatus( getStatus(
userUuid: string, userUuid: string,
transitionType: 'items' | 'revisions', transitionType: 'items' | 'revisions',
): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null> ): Promise<'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null>
getStatuses(
transitionType: 'items' | 'revisions',
): Promise<Array<{ userUuid: string; status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' }>>
} }
@@ -35,7 +35,7 @@ export class GenerateAuthenticatorAuthenticationOptions
.update(`u2f-selector-${dto.username}${this.pseudoKeyParamsKey}`) .update(`u2f-selector-${dto.username}${this.pseudoKeyParamsKey}`)
.digest('base64url') .digest('base64url')
const options = generateAuthenticationOptions({ const options = await generateAuthenticationOptions({
allowCredentials: [ allowCredentials: [
{ {
id: Buffer.from(credentialIdHash), id: Buffer.from(credentialIdHash),
@@ -56,7 +56,7 @@ export class GenerateAuthenticatorAuthenticationOptions
const userUuid = userUuidOrError.getValue() const userUuid = userUuidOrError.getValue()
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid) const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
const options = generateAuthenticationOptions({ const options = await generateAuthenticationOptions({
allowCredentials: authenticators.map((authenticator) => ({ allowCredentials: authenticators.map((authenticator) => ({
id: authenticator.props.credentialId, id: authenticator.props.credentialId,
type: 'public-key', type: 'public-key',
@@ -52,7 +52,7 @@ export class GenerateAuthenticatorRegistrationOptions
} }
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid) const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
const options = generateRegistrationOptions({ const options = await generateRegistrationOptions({
rpID: this.relyingPartyId, rpID: this.relyingPartyId,
rpName: this.relyingPartyName, rpName: this.relyingPartyName,
userID: userUuid.value, 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,36 +3,37 @@ import { RoleName, Uuid } from '@standardnotes/domain-core'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface' import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface' import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatus } from './UpdateTransitionStatus' import { UpdateTransitionStatus } from './UpdateTransitionStatus'
import { Logger } from 'winston'
describe('UpdateTransitionStatus', () => { describe('UpdateTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface let transitionStatusRepository: TransitionStatusRepositoryInterface
let roleService: RoleServiceInterface let roleService: RoleServiceInterface
let logger: Logger
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService) const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService, logger)
beforeEach(() => { beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface> transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.removeStatus = jest.fn()
transitionStatusRepository.updateStatus = jest.fn() transitionStatusRepository.updateStatus = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addRoleToUser = jest.fn() 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 useCase = createUseCase()
const result = await useCase.execute({ const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000', userUuid: '00000000-0000-0000-0000-000000000000',
status: 'FINISHED', status: 'VERIFIED',
transitionType: 'items', transitionType: 'items',
transitionTimestamp: 123,
}) })
expect(result.isFailed()).toBeFalsy() expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000',
'items',
)
expect(roleService.addRoleToUser).toHaveBeenCalledWith( expect(roleService.addRoleToUser).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
RoleName.create(RoleName.NAMES.TransitionUser).getValue(), RoleName.create(RoleName.NAMES.TransitionUser).getValue(),
@@ -46,6 +47,7 @@ describe('UpdateTransitionStatus', () => {
userUuid: '00000000-0000-0000-0000-000000000000', userUuid: '00000000-0000-0000-0000-000000000000',
status: 'STARTED', status: 'STARTED',
transitionType: 'items', transitionType: 'items',
transitionTimestamp: 123,
}) })
expect(result.isFailed()).toBeFalsy() expect(result.isFailed()).toBeFalsy()
@@ -63,6 +65,7 @@ describe('UpdateTransitionStatus', () => {
userUuid: 'invalid', userUuid: 'invalid',
status: 'STARTED', status: 'STARTED',
transitionType: 'items', transitionType: 'items',
transitionTimestamp: 123,
}) })
expect(result.isFailed()).toBeTruthy() expect(result.isFailed()).toBeTruthy()
@@ -2,11 +2,13 @@ import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface' import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatusDTO } from './UpdateTransitionStatusDTO' import { UpdateTransitionStatusDTO } from './UpdateTransitionStatusDTO'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface' import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { Logger } from 'winston'
export class UpdateTransitionStatus implements UseCaseInterface<void> { export class UpdateTransitionStatus implements UseCaseInterface<void> {
constructor( constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface, private transitionStatusRepository: TransitionStatusRepositoryInterface,
private roleService: RoleServiceInterface, private roleService: RoleServiceInterface,
private logger: Logger,
) {} ) {}
async execute(dto: UpdateTransitionStatusDTO): Promise<Result<void>> { async execute(dto: UpdateTransitionStatusDTO): Promise<Result<void>> {
@@ -16,16 +18,14 @@ export class UpdateTransitionStatus implements UseCaseInterface<void> {
} }
const userUuid = userUuidOrError.getValue() const userUuid = userUuidOrError.getValue()
if (dto.status === 'FINISHED') { this.logger.info(`Received transition status updated event to ${dto.status} for user ${dto.userUuid}`)
await this.transitionStatusRepository.removeStatus(dto.userUuid, dto.transitionType)
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
return Result.ok()
}
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status) await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
if (dto.transitionType === 'items' && dto.status === 'VERIFIED') {
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
}
return Result.ok() return Result.ok()
} }
} }
@@ -1,5 +1,6 @@
export interface UpdateTransitionStatusDTO { export interface UpdateTransitionStatusDTO {
userUuid: string userUuid: string
transitionType: 'items' | 'revisions' transitionType: 'items' | 'revisions'
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' transitionTimestamp: number
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 itemStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
private revisionStatuses: 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( async updateStatus(
userUuid: string, userUuid: string,
transitionType: 'items' | 'revisions', transitionType: 'items' | 'revisions',
@@ -14,7 +14,6 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription' import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { UpdateUser } from '../../Domain/UseCase/UpdateUser' import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
import { User } from '../../Domain/User/User' import { User } from '../../Domain/User/User'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
describe('AnnotatedUsersController', () => { describe('AnnotatedUsersController', () => {
let updateUser: UpdateUser let updateUser: UpdateUser
@@ -25,7 +24,6 @@ describe('AnnotatedUsersController', () => {
let increaseLoginAttempts: IncreaseLoginAttempts let increaseLoginAttempts: IncreaseLoginAttempts
let changeCredentials: ChangeCredentials let changeCredentials: ChangeCredentials
let inviteToSharedSubscription: InviteToSharedSubscription let inviteToSharedSubscription: InviteToSharedSubscription
let getTransitionStatus: GetTransitionStatus
let request: express.Request let request: express.Request
let response: express.Response let response: express.Response
@@ -40,7 +38,6 @@ describe('AnnotatedUsersController', () => {
clearLoginAttempts, clearLoginAttempts,
increaseLoginAttempts, increaseLoginAttempts,
changeCredentials, changeCredentials,
getTransitionStatus,
) )
beforeEach(() => { beforeEach(() => {
@@ -72,9 +69,6 @@ describe('AnnotatedUsersController', () => {
inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription> inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription>
inviteToSharedSubscription.execute = jest.fn() inviteToSharedSubscription.execute = jest.fn()
getTransitionStatus = {} as jest.Mocked<GetTransitionStatus>
getTransitionStatus.execute = jest.fn()
request = { request = {
headers: {}, headers: {},
body: {}, body: {},
@@ -18,7 +18,6 @@ import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts' import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials' import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { BaseUsersController } from './Base/BaseUsersController' import { BaseUsersController } from './Base/BaseUsersController'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
@controller('/users') @controller('/users')
export class AnnotatedUsersController extends BaseUsersController { export class AnnotatedUsersController extends BaseUsersController {
@@ -30,7 +29,6 @@ export class AnnotatedUsersController extends BaseUsersController {
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts, @inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts, @inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials, @inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
@inject(TYPES.Auth_GetTransitionStatus) override getTransitionStatusUseCase: GetTransitionStatus,
) { ) {
super( super(
updateUser, updateUser,
@@ -40,7 +38,6 @@ export class AnnotatedUsersController extends BaseUsersController {
clearLoginAttempts, clearLoginAttempts,
increaseLoginAttempts, increaseLoginAttempts,
changeCredentialsUseCase, changeCredentialsUseCase,
getTransitionStatusUseCase,
) )
} }
@@ -54,11 +51,6 @@ export class AnnotatedUsersController extends BaseUsersController {
return super.keyParams(request) 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) @httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> { override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
return super.deleteAccount(request, response) return super.deleteAccount(request, response)
@@ -10,7 +10,6 @@ import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts' import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser' import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
import { ErrorTag } from '@standardnotes/responses' import { ErrorTag } from '@standardnotes/responses'
import { GetTransitionStatus } from '../../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
export class BaseUsersController extends BaseHttpController { export class BaseUsersController extends BaseHttpController {
constructor( constructor(
@@ -21,7 +20,6 @@ export class BaseUsersController extends BaseHttpController {
protected clearLoginAttempts: ClearLoginAttempts, protected clearLoginAttempts: ClearLoginAttempts,
protected increaseLoginAttempts: IncreaseLoginAttempts, protected increaseLoginAttempts: IncreaseLoginAttempts,
protected changeCredentialsUseCase: ChangeCredentials, protected changeCredentialsUseCase: ChangeCredentials,
protected getTransitionStatusUseCase: GetTransitionStatus,
private controllerContainer?: ControllerContainerInterface, private controllerContainer?: ControllerContainerInterface,
) { ) {
super() 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.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.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.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) 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> { async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) { if (request.params.userUuid !== response.locals.user.uuid) {
return this.json( return this.json(
@@ -7,47 +7,35 @@ export class RedisTransitionStatusRepository implements TransitionStatusReposito
constructor(private redisClient: IORedis.Redis) {} 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( async updateStatus(
userUuid: string, userUuid: string,
transitionType: 'items' | 'revisions', transitionType: 'items' | 'revisions',
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED', status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED',
): Promise<void> { ): Promise<void> {
if (status === 'IN_PROGRESS') { switch (status) {
await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, 7200, status) case 'FAILED':
} else { case 'VERIFIED':
await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status) 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( async getStatus(
userUuid: string, userUuid: string,
transitionType: 'items' | 'revisions', transitionType: 'items' | 'revisions',
): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null> { ): Promise<'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null> {
const status = (await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)) as const status = await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)
| 'STARTED'
| 'IN_PROGRESS'
| 'FAILED'
| null
return status return status as 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED' | null
} }
} }
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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.12.27](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.26...@standardnotes/domain-events-infra@1.12.27) (2023-09-12) ## [1.12.27](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.26...@standardnotes/domain-events-infra@1.12.27) (2023-09-12)
**Note:** Version bump only for package @standardnotes/domain-events-infra **Note:** Version bump only for package @standardnotes/domain-events-infra
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/domain-events-infra", "name": "@standardnotes/domain-events-infra",
"version": "1.12.27", "version": "1.12.29",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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
* display transition progress in logs ([38685c1](https://github.com/standardnotes/server/commit/38685c1861b13e398dd96aa39f2cf1aece2090fb))
## [2.125.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.125.2...@standardnotes/domain-events@2.125.3) (2023-09-12) ## [2.125.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.125.2...@standardnotes/domain-events@2.125.3) (2023-09-12)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/domain-events", "name": "@standardnotes/domain-events",
"version": "2.125.3", "version": "2.126.0",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
@@ -1,4 +1,5 @@
export interface TransitionRequestedEventPayload { export interface TransitionRequestedEventPayload {
userUuid: string userUuid: string
type: 'items' | 'revisions' type: 'items' | 'revisions'
timestamp: number
} }
@@ -1,5 +1,6 @@
export interface TransitionStatusUpdatedEventPayload { export interface TransitionStatusUpdatedEventPayload {
userUuid: string userUuid: string
transitionType: 'items' | 'revisions' transitionType: 'items' | 'revisions'
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED' | 'VERIFIED'
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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.11.39](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.38...@standardnotes/event-store@1.11.39) (2023-09-12) ## [1.11.39](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.38...@standardnotes/event-store@1.11.39) (2023-09-12)
**Note:** Version bump only for package @standardnotes/event-store **Note:** Version bump only for package @standardnotes/event-store
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/event-store", "name": "@standardnotes/event-store",
"version": "1.11.39", "version": "1.11.41",
"description": "Event Store Service", "description": "Event Store Service",
"private": true, "private": true,
"main": "dist/src/index.js", "main": "dist/src/index.js",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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.22.18](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.17...@standardnotes/files-server@1.22.18) (2023-09-12) ## [1.22.18](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.17...@standardnotes/files-server@1.22.18) (2023-09-12)
**Note:** Version bump only for package @standardnotes/files-server **Note:** Version bump only for package @standardnotes/files-server
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/files-server", "name": "@standardnotes/files-server",
"version": "1.22.18", "version": "1.22.20",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+56
View File
@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.15.57](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.56...@standardnotes/home-server@1.15.57) (2023-09-15)
**Note:** Version bump only for package @standardnotes/home-server
## [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.15.48](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.47...@standardnotes/home-server@1.15.48) (2023-09-13)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.47](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.46...@standardnotes/home-server@1.15.47) (2023-09-13)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.46](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.45...@standardnotes/home-server@1.15.46) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.45](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.44...@standardnotes/home-server@1.15.45) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.44](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.43...@standardnotes/home-server@1.15.44) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.43](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.42...@standardnotes/home-server@1.15.43) (2023-09-12) ## [1.15.43](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.42...@standardnotes/home-server@1.15.43) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server **Note:** Version bump only for package @standardnotes/home-server
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/home-server", "name": "@standardnotes/home-server",
"version": "1.15.43", "version": "1.15.57",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+60
View File
@@ -3,6 +3,66 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.34.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.34.0...@standardnotes/revisions-server@1.34.1) (2023-09-15)
### Bug Fixes
* add debug logs for transition status updates ([3e7856c](https://github.com/standardnotes/server/commit/3e7856c895e73b775c8977c6c6e86dffd5755c00))
# [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
* display transition progress in logs ([38685c1](https://github.com/standardnotes/server/commit/38685c1861b13e398dd96aa39f2cf1aece2090fb))
## [1.33.17](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.16...@standardnotes/revisions-server@1.33.17) (2023-09-13)
### Bug Fixes
* setting status for already migrated users ([9be4c00](https://github.com/standardnotes/server/commit/9be4c002b755fea057489b6077b297162223aefe))
## [1.33.16](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.15...@standardnotes/revisions-server@1.33.16) (2023-09-13)
### Bug Fixes
* cleanup only for 0 new items ([b1d88b1](https://github.com/standardnotes/server/commit/b1d88b15be78a48224963e337a222fb675ed2692))
## [1.33.15](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.14...@standardnotes/revisions-server@1.33.15) (2023-09-12)
### Bug Fixes
* sync between primary and secondary database with diff ([fab5d18](https://github.com/standardnotes/server/commit/fab5d180645e0a6fa0c9c67205d44f27c8a65c8b))
## [1.33.14](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.13...@standardnotes/revisions-server@1.33.14) (2023-09-12)
### Bug Fixes
* **revisions:** handle transitions with already existing data in secondary ([aa83526](https://github.com/standardnotes/server/commit/aa835268ea80e3aa74907e449d189e8b2774a859))
## [1.33.13](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.12...@standardnotes/revisions-server@1.33.13) (2023-09-12) ## [1.33.13](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.12...@standardnotes/revisions-server@1.33.13) (2023-09-12)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/revisions-server", "name": "@standardnotes/revisions-server",
"version": "1.33.13", "version": "1.34.1",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
@@ -443,6 +443,7 @@ export class ContainerConfigLoader {
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Revisions_TransitionStatusUpdatedEventHandler) .bind<TransitionStatusUpdatedEventHandler>(TYPES.Revisions_TransitionStatusUpdatedEventHandler)
.toConstantValue( .toConstantValue(
new TransitionStatusUpdatedEventHandler( new TransitionStatusUpdatedEventHandler(
container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository),
container.get<TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser>( container.get<TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser, TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
), ),
@@ -519,9 +520,6 @@ export class ContainerConfigLoader {
container.get<DeleteRevision>(TYPES.Revisions_DeleteRevision), container.get<DeleteRevision>(TYPES.Revisions_DeleteRevision),
container.get<RevisionHttpMapper>(TYPES.Revisions_RevisionHttpMapper), container.get<RevisionHttpMapper>(TYPES.Revisions_RevisionHttpMapper),
container.get<RevisionMetadataHttpMapper>(TYPES.Revisions_RevisionMetadataHttpMapper), container.get<RevisionMetadataHttpMapper>(TYPES.Revisions_RevisionMetadataHttpMapper),
container.get<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
),
container.get<ControllerContainerInterface>(TYPES.Revisions_ControllerContainer), container.get<ControllerContainerInterface>(TYPES.Revisions_ControllerContainer),
), ),
) )
@@ -9,7 +9,8 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: { createTransitionStatusUpdatedEvent(dto: {
userUuid: string userUuid: string
transitionType: 'items' | 'revisions' transitionType: 'items' | 'revisions'
status: 'STARTED' | 'FAILED' | 'FINISHED' transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent { }): TransitionStatusUpdatedEvent {
return { return {
type: 'TRANSITION_STATUS_UPDATED', type: 'TRANSITION_STATUS_UPDATED',
@@ -19,7 +20,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
userIdentifier: dto.userUuid, userIdentifier: dto.userUuid,
userIdentifierType: 'uuid', userIdentifierType: 'uuid',
}, },
origin: DomainEventService.SyncingServer, origin: DomainEventService.Revisions,
}, },
payload: dto, payload: dto,
} }
@@ -4,6 +4,7 @@ export interface DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: { createTransitionStatusUpdatedEvent(dto: {
userUuid: string userUuid: string
transitionType: 'items' | 'revisions' transitionType: 'items' | 'revisions'
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent }): TransitionStatusUpdatedEvent
} }
@@ -18,6 +18,7 @@ export class TransitionRequestedEventHandler implements DomainEventHandlerInterf
const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({ const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
transitionTimestamp: event.payload.timestamp,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -6,9 +6,12 @@ import {
import { Logger } from 'winston' import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from '../UseCase/Transition/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser' import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from '../UseCase/Transition/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser'
import { Uuid } from '@standardnotes/domain-core'
import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface { export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
constructor( constructor(
private primaryRevisionsRepository: RevisionRepositoryInterface,
private transitionRevisionsFromPrimaryToSecondaryDatabaseForUser: TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser, private transitionRevisionsFromPrimaryToSecondaryDatabaseForUser: TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
private domainEventPublisher: DomainEventPublisherInterface, private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface, private domainEventFactory: DomainEventFactoryInterface,
@@ -22,18 +25,42 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
status: 'STARTED', status: 'STARTED',
transitionType: 'revisions', transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}), }),
) )
return return
} }
this.logger.info(
`Received transition status updated event to ${event.payload.status} for user ${event.payload.userUuid}`,
)
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'revisions') { if (event.payload.status === 'STARTED' && event.payload.transitionType === 'revisions') {
const userUuid = await this.getUserUuidFromEvent(event)
if (userUuid === null) {
return
}
if (await this.isAlreadyMigrated(userUuid)) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'FINISHED',
transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}),
)
return
}
await this.domainEventPublisher.publish( await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({ this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
status: 'IN_PROGRESS', status: 'IN_PROGRESS',
transitionType: 'revisions', transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}), }),
) )
@@ -49,6 +76,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
status: 'FAILED', status: 'FAILED',
transitionType: 'revisions', transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}), }),
) )
@@ -60,10 +88,60 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
status: 'FINISHED', status: 'FINISHED',
transitionType: 'revisions', transitionType: 'revisions',
transitionTimestamp: event.payload.transitionTimestamp,
}), }),
) )
return 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> {
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
if (totalRevisionsCountForUserInPrimary > 0) {
this.logger.info(
`User ${userUuid.value} has ${totalRevisionsCountForUserInPrimary} revisions in primary database.`,
)
}
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()
} }
} }
@@ -1,455 +0,0 @@
import { Logger } from 'winston'
import { RevisionRepositoryInterface } from '../../../Revision/RevisionRepositoryInterface'
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from './TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser'
import { Revision } from '../../../Revision/Revision'
import { ContentType, Dates, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
describe('TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser', () => {
let primaryRevisionRepository: RevisionRepositoryInterface
let secondaryRevisionRepository: RevisionRepositoryInterface | null
let logger: Logger
let primaryRevision1: Revision
let primaryRevision2: Revision
let secondaryRevision1: Revision
let secondaryRevision2: Revision
let timer: TimerInterface
const createUseCase = () =>
new TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser(
primaryRevisionRepository,
secondaryRevisionRepository,
timer,
logger,
1,
)
beforeEach(() => {
primaryRevision1 = Revision.create(
{
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
content: 'test',
contentType: ContentType.create('Note').getValue(),
itemsKeyId: 'test',
encItemKey: 'test',
authHash: 'test',
creationDate: new Date(1),
dates: Dates.create(new Date(1), new Date(2)).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
primaryRevision2 = Revision.create(
{
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc2d').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
content: 'test',
contentType: ContentType.create('Note').getValue(),
itemsKeyId: 'test',
encItemKey: 'test',
authHash: 'test',
creationDate: new Date(1),
dates: Dates.create(new Date(1), new Date(2)).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
).getValue()
secondaryRevision1 = Revision.create(
{
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
content: 'test',
contentType: ContentType.create('Note').getValue(),
itemsKeyId: 'test',
encItemKey: 'test',
authHash: 'test',
creationDate: new Date(1),
dates: Dates.create(new Date(1), new Date(2)).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
secondaryRevision2 = Revision.create(
{
itemUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc2d').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
content: 'test',
contentType: ContentType.create('Note').getValue(),
itemsKeyId: 'test',
encItemKey: 'test',
authHash: 'test',
creationDate: new Date(1),
dates: Dates.create(new Date(1), new Date(2)).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
).getValue()
primaryRevisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
primaryRevisionRepository.countByUserUuid = jest.fn().mockResolvedValue(2)
primaryRevisionRepository.findByUserUuid = jest
.fn()
.mockResolvedValueOnce([primaryRevision1])
.mockResolvedValueOnce([primaryRevision2])
.mockResolvedValueOnce([primaryRevision1])
.mockResolvedValueOnce([primaryRevision2])
primaryRevisionRepository.removeByUserUuid = jest.fn().mockResolvedValue(undefined)
secondaryRevisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
secondaryRevisionRepository.insert = jest.fn().mockResolvedValue(true)
secondaryRevisionRepository.removeByUserUuid = jest.fn().mockResolvedValue(undefined)
secondaryRevisionRepository.countByUserUuid = jest.fn().mockResolvedValueOnce(0).mockResolvedValueOnce(2)
secondaryRevisionRepository.findOneByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryRevision1)
.mockResolvedValueOnce(secondaryRevision2)
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
logger.info = jest.fn()
logger.debug = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.sleep = jest.fn()
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
timer.convertMicrosecondsToTimeStructure = jest.fn().mockReturnValue({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
})
})
describe('successfull transition', () => {
it('should transition Revisions from primary to secondary database', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(3)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenCalledTimes(4)
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(1, {
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
limit: 1,
offset: 0,
})
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(2, {
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
limit: 1,
offset: 1,
})
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(3, {
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
limit: 1,
offset: 0,
})
expect(primaryRevisionRepository.findByUserUuid).toHaveBeenNthCalledWith(4, {
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
limit: 1,
offset: 1,
})
expect((secondaryRevisionRepository as RevisionRepositoryInterface).insert).toHaveBeenCalledTimes(2)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).insert).toHaveBeenCalledWith(primaryRevision1)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).insert).toHaveBeenCalledWith(primaryRevision2)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).not.toHaveBeenCalled()
expect(primaryRevisionRepository.removeByUserUuid).toHaveBeenCalledTimes(1)
})
it('should log an error if deleting Revisions from primary database fails', async () => {
primaryRevisionRepository.removeByUserUuid = jest.fn().mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up primary database revisions for user 00000000-0000-0000-0000-000000000000: Errored when deleting revisions for user 00000000-0000-0000-0000-000000000000: error',
)
})
})
describe('failed transition', () => {
it('should remove Revisions from secondary database if integrity check fails', async () => {
const secondaryRevision2WithDifferentContent = Revision.create({
...secondaryRevision2.props,
content: 'different-content',
}).getValue()
;(secondaryRevisionRepository as RevisionRepositoryInterface).findOneByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryRevision1)
.mockResolvedValueOnce(secondaryRevision2WithDifferentContent)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
})
it('should remove Revisions from secondary database if migrating Revisions fails', async () => {
primaryRevisionRepository.findByUserUuid = jest
.fn()
.mockResolvedValueOnce([primaryRevision1])
.mockRejectedValueOnce(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Errored when migrating revisions for user 00000000-0000-0000-0000-000000000000: error',
)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
})
it('should return an error for a specific revision if it errors when saving to secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).insert = jest
.fn()
.mockResolvedValueOnce(true)
.mockRejectedValueOnce(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Errored when saving revision 00000000-0000-0000-0000-000000000001 to secondary database: error',
)
})
it('should log an error if deleting Revisions from secondary database fails upon migration failure', async () => {
primaryRevisionRepository.findByUserUuid = jest
.fn()
.mockResolvedValueOnce([primaryRevision1])
.mockRejectedValueOnce(new Error('error'))
;(secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid = jest
.fn()
.mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up secondary database revisions for user 00000000-0000-0000-0000-000000000000: Errored when deleting revisions for user 00000000-0000-0000-0000-000000000000: error',
)
})
it('should log an error if deleting Revisions from secondary database fails upon integrity check failure', async () => {
const secondaryRevision2WithDifferentContent = Revision.create({
...secondaryRevision2.props,
content: 'different-content',
}).getValue()
;(secondaryRevisionRepository as RevisionRepositoryInterface).findOneByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryRevision1)
.mockResolvedValueOnce(secondaryRevision2WithDifferentContent)
;(secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid = jest
.fn()
.mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up secondary database revisions for user 00000000-0000-0000-0000-000000000000: Errored when deleting revisions for user 00000000-0000-0000-0000-000000000000: error',
)
})
it('should not perform the transition if secondary Revision repository is not set', async () => {
secondaryRevisionRepository = null
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Secondary revision repository is not set')
expect(primaryRevisionRepository.countByUserUuid).not.toHaveBeenCalled()
expect(primaryRevisionRepository.findByUserUuid).not.toHaveBeenCalled()
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
})
it('should not perform the transition if the user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid-uuid',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid-uuid')
expect(primaryRevisionRepository.countByUserUuid).not.toHaveBeenCalled()
expect(primaryRevisionRepository.findByUserUuid).not.toHaveBeenCalled()
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
})
it('should fail integrity check if the Revision count is not the same in both databases', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid = jest
.fn()
.mockResolvedValueOnce(0)
.mockResolvedValueOnce(1)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Total revisions count for user 00000000-0000-0000-0000-000000000000 in primary database (2) does not match total revisions count in secondary database (1)',
)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(3)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid).toHaveBeenCalledTimes(2)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if one Revision is not found in the secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).findOneByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryRevision1)
.mockResolvedValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Revision 00000000-0000-0000-0000-000000000001 not found in secondary database')
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(3)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if an error is thrown during integrity check between primary and secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid = jest
.fn()
.mockReturnValueOnce(0)
.mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Errored when checking integrity between primary and secondary database: error')
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if a revisions did not save in the secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).insert = jest.fn().mockResolvedValue(false)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Failed to save revision 00000000-0000-0000-0000-000000000000 to secondary database',
)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
})
it('should not migrate revisions if there are no revisions in the primary database', async () => {
primaryRevisionRepository.countByUserUuid = jest.fn().mockResolvedValue(0)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect(primaryRevisionRepository.findByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).insert).not.toHaveBeenCalled()
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).not.toHaveBeenCalled()
})
it('should not migrate revisions if there are already revisions in the secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid = jest.fn().mockResolvedValueOnce(1)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(primaryRevisionRepository.findByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).insert).not.toHaveBeenCalled()
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).not.toHaveBeenCalled()
})
})
@@ -1,9 +1,11 @@
/* istanbul ignore file */
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston' import { Logger } from 'winston'
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO } from './TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO' import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO } from './TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO'
import { RevisionRepositoryInterface } from '../../../Revision/RevisionRepositoryInterface' import { RevisionRepositoryInterface } from '../../../Revision/RevisionRepositoryInterface'
import { Revision } from '../../../Revision/Revision'
export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> { export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
constructor( constructor(
@@ -27,27 +29,54 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
} }
const userUuid = userUuidOrError.getValue() const userUuid = userUuidOrError.getValue()
if (await this.isAlreadyMigrated(userUuid)) { let newRevisionsInSecondaryCount = 0
this.logger.info(`Revisions for user ${userUuid.value} are already migrated`) let updatedRevisionsInSecondary: Revision[] = []
return Result.ok()
}
if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) { if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
return Result.fail(`Revisions for user ${userUuid.value} already exist in secondary database`) 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(),
userUuid,
)
}
if (newRevisionsInSecondary.length > 0) {
this.logger.info(
`Found ${newRevisionsInSecondary.length} new revisions in secondary database for user ${userUuid.value}`,
)
}
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() const migrationTimeStart = this.timer.getTimestampInMicroseconds()
this.logger.debug(`Transitioning revisions for user ${userUuid.value}`) 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 (migrationResult.isFailed()) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository) if (newRevisionsInSecondaryCount === 0 && updatedRevisionsInSecondaryCount === 0) {
if (cleanupResult.isFailed()) { const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
this.logger.error( if (cleanupResult.isFailed()) {
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`, this.logger.error(
) `Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
}
} }
return Result.fail(migrationResult.getError()) return Result.fail(migrationResult.getError())
@@ -55,13 +84,19 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
await this.allowForSecondaryDatabaseToCatchUp() await this.allowForSecondaryDatabaseToCatchUp()
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid) const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid,
newRevisionsInSecondaryCount,
updatedRevisionsInSecondary,
)
if (integrityCheckResult.isFailed()) { if (integrityCheckResult.isFailed()) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository) if (newRevisionsInSecondaryCount === 0 && updatedRevisionsInSecondaryCount === 0) {
if (cleanupResult.isFailed()) { const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
this.logger.error( if (cleanupResult.isFailed()) {
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`, this.logger.error(
) `Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
}
} }
return Result.fail(integrityCheckResult.getError()) return Result.fail(integrityCheckResult.getError())
@@ -86,7 +121,10 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
return Result.ok() return Result.ok()
} }
private async migrateRevisionsForUser(userUuid: Uuid): Promise<Result<void>> { private async migrateRevisionsForUser(
userUuid: Uuid,
updatedRevisionsInSecondary: Revision[],
): Promise<Result<void>> {
try { try {
const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid) const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
let totalRevisionsCountTransitionedToSecondary = 0 let totalRevisionsCountTransitionedToSecondary = 0
@@ -102,6 +140,18 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
for (const revision of revisions) { for (const revision of revisions) {
try { 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( this.logger.debug(
`Transitioning revision #${ `Transitioning revision #${
totalRevisionsCountTransitionedToSecondary + 1 totalRevisionsCountTransitionedToSecondary + 1
@@ -154,16 +204,94 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
this.secondRevisionsRepository as RevisionRepositoryInterface this.secondRevisionsRepository as RevisionRepositoryInterface
).countByUserUuid(userUuid) ).countByUserUuid(userUuid)
return totalRevisionsCountForUserInSecondary > 0 const hasAlreadyDataInSecondaryDatabase = totalRevisionsCountForUserInSecondary > 0
if (hasAlreadyDataInSecondaryDatabase) {
this.logger.info(
`User ${userUuid.value} has already ${totalRevisionsCountForUserInSecondary} revisions in secondary database`,
)
}
return hasAlreadyDataInSecondaryDatabase
} }
private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> { private async getNewRevisionsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid) alreadyExistingInPrimary: Revision[]
newRevisionsInSecondary: Revision[]
updatedInSecondary: Revision[]
}> {
const revisions = await (this.secondRevisionsRepository as RevisionRepositoryInterface).findByUserUuid({
userUuid: userUuid,
})
return totalRevisionsCountForUserInPrimary === 0 const alreadyExistingInPrimary: Revision[] = []
const newRevisionsInSecondary: Revision[] = []
const updatedInSecondary: Revision[] = []
for (const revision of revisions) {
const { revisionInPrimary, newerRevisionInSecondary } =
await this.checkIfRevisionExistsInPrimaryDatabase(revision)
if (revisionInPrimary !== null) {
alreadyExistingInPrimary.push(revision)
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 checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid: Uuid): Promise<Result<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,
[],
)
if (revisionInPrimary === null) {
return {
revisionInPrimary: null,
newerRevisionInSecondary: null,
}
}
if (!revision.isIdenticalTo(revisionInPrimary)) {
this.logger.error(
`Revision ${revision.id.toString()} is not identical in primary and secondary database. Revision in secondary database: ${JSON.stringify(
revision,
)}, revision in primary database: ${JSON.stringify(revisionInPrimary)}`,
)
return {
revisionInPrimary: null,
newerRevisionInSecondary:
revision.props.dates.updatedAt > revisionInPrimary.props.dates.updatedAt ? revision : null,
}
}
return {
revisionInPrimary: revisionInPrimary,
newerRevisionInSecondary: null,
}
}
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid: Uuid,
newRevisionsInSecondaryCount: number,
updatedRevisionsInSecondary: Revision[],
): Promise<Result<boolean>> {
try { try {
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid) const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
@@ -192,6 +320,17 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
return Result.fail(`Revision ${revision.id.toString()} not found in secondary database`) 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)) { if (!revision.isIdenticalTo(revisionInSecondary)) {
return Result.fail( return Result.fail(
`Revision ${revision.id.toString()} is not identical in primary and secondary database. Revision in primary database: ${JSON.stringify( `Revision ${revision.id.toString()} is not identical in primary and secondary database. Revision in primary database: ${JSON.stringify(
@@ -206,9 +345,12 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
this.secondRevisionsRepository as RevisionRepositoryInterface this.secondRevisionsRepository as RevisionRepositoryInterface
).countByUserUuid(userUuid) ).countByUserUuid(userUuid)
if (totalRevisionsCountForUserInPrimary !== totalRevisionsCountForUserInSecondary) { if (
totalRevisionsCountForUserInPrimary + newRevisionsInSecondaryCount !==
totalRevisionsCountForUserInSecondary
) {
return Result.fail( return Result.fail(
`Total revisions count for user ${userUuid.value} in primary database (${totalRevisionsCountForUserInPrimary}) does not match total revisions count in secondary database (${totalRevisionsCountForUserInSecondary})`, `Total revisions count for user ${userUuid.value} in primary database (${totalRevisionsCountForUserInPrimary} + ${newRevisionsInSecondaryCount}) does not match total revisions count in secondary database (${totalRevisionsCountForUserInSecondary})`,
) )
} }
@@ -23,6 +23,7 @@ describe('TriggerTransitionFromPrimaryToSecondaryDatabaseForUser', () => {
await useCase.execute({ await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000', userUuid: '00000000-0000-0000-0000-000000000000',
transitionTimestamp: 123,
}) })
expect(domainEventPubliser.publish).toHaveBeenCalled() expect(domainEventPubliser.publish).toHaveBeenCalled()
@@ -15,6 +15,7 @@ export class TriggerTransitionFromPrimaryToSecondaryDatabaseForUser implements U
userUuid: dto.userUuid, userUuid: dto.userUuid,
status: 'STARTED', status: 'STARTED',
transitionType: 'revisions', transitionType: 'revisions',
transitionTimestamp: dto.transitionTimestamp,
}) })
await this.domainEventPubliser.publish(event) await this.domainEventPubliser.publish(event)
@@ -1,3 +1,4 @@
export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO { export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO {
userUuid: string userUuid: string
transitionTimestamp: number
} }
@@ -1,5 +1,5 @@
import { Request, Response } from 'express' 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 { inject } from 'inversify'
import TYPES from '../../Bootstrap/Types' import TYPES from '../../Bootstrap/Types'
@@ -12,7 +12,6 @@ import { Revision } from '../../Domain/Revision/Revision'
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata' import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
import { RevisionHttpRepresentation } from '../../Mapping/Http/RevisionHttpRepresentation' import { RevisionHttpRepresentation } from '../../Mapping/Http/RevisionHttpRepresentation'
import { RevisionMetadataHttpRepresentation } from '../../Mapping/Http/RevisionMetadataHttpRepresentation' import { RevisionMetadataHttpRepresentation } from '../../Mapping/Http/RevisionMetadataHttpRepresentation'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
@controller('', TYPES.Revisions_ApiGatewayAuthMiddleware) @controller('', TYPES.Revisions_ApiGatewayAuthMiddleware)
export class AnnotatedRevisionsController extends BaseRevisionsController { export class AnnotatedRevisionsController extends BaseRevisionsController {
@@ -24,17 +23,8 @@ export class AnnotatedRevisionsController extends BaseRevisionsController {
override revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>, override revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>,
@inject(TYPES.Revisions_RevisionMetadataHttpMapper) @inject(TYPES.Revisions_RevisionMetadataHttpMapper)
override revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>, override revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>,
@inject(TYPES.Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser)
override triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
) { ) {
super( super(getRevisionsMetadata, doGetRevision, doDeleteRevision, revisionHttpMapper, revisionMetadataHttpMapper)
getRevisionsMetadata,
doGetRevision,
doDeleteRevision,
revisionHttpMapper,
revisionMetadataHttpMapper,
triggerTransitionFromPrimaryToSecondaryDatabaseForUser,
)
} }
@httpGet('/items/:itemUuid/revisions') @httpGet('/items/:itemUuid/revisions')
@@ -51,9 +41,4 @@ export class AnnotatedRevisionsController extends BaseRevisionsController {
override async deleteRevision(request: Request, response: Response): Promise<results.JsonResult> { override async deleteRevision(request: Request, response: Response): Promise<results.JsonResult> {
return super.deleteRevision(request, response) 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 { GetRevisionsMetada } from '../../../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
import { RevisionHttpRepresentation } from '../../../Mapping/Http/RevisionHttpRepresentation' import { RevisionHttpRepresentation } from '../../../Mapping/Http/RevisionHttpRepresentation'
import { RevisionMetadataHttpRepresentation } from '../../../Mapping/Http/RevisionMetadataHttpRepresentation' import { RevisionMetadataHttpRepresentation } from '../../../Mapping/Http/RevisionMetadataHttpRepresentation'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
export class BaseRevisionsController extends BaseHttpController { export class BaseRevisionsController extends BaseHttpController {
constructor( constructor(
@@ -20,7 +19,6 @@ export class BaseRevisionsController extends BaseHttpController {
protected doDeleteRevision: DeleteRevision, protected doDeleteRevision: DeleteRevision,
protected revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>, protected revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>,
protected revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>, protected revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>,
protected triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
private controllerContainer?: ControllerContainerInterface, private controllerContainer?: ControllerContainerInterface,
) { ) {
super() 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.getRevisions', this.getRevisions.bind(this))
this.controllerContainer.register('revisions.revisions.getRevision', this.getRevision.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.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(), 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 })
}
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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.20.43](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.42...@standardnotes/scheduler-server@1.20.43) (2023-09-12) ## [1.20.43](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.42...@standardnotes/scheduler-server@1.20.43) (2023-09-12)
**Note:** Version bump only for package @standardnotes/scheduler-server **Note:** Version bump only for package @standardnotes/scheduler-server
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/scheduler-server", "name": "@standardnotes/scheduler-server",
"version": "1.20.43", "version": "1.20.45",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+69
View File
@@ -3,6 +3,75 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.96.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.96.0...@standardnotes/syncing-server@1.96.1) (2023-09-15)
### Bug Fixes
* add debug logs for transition status updates ([3e7856c](https://github.com/standardnotes/syncing-server-js/commit/3e7856c895e73b775c8977c6c6e86dffd5755c00))
# [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
* display transition progress in logs ([38685c1](https://github.com/standardnotes/syncing-server-js/commit/38685c1861b13e398dd96aa39f2cf1aece2090fb))
## [1.95.14](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.13...@standardnotes/syncing-server@1.95.14) (2023-09-13)
### Bug Fixes
* setting status for already migrated users ([9be4c00](https://github.com/standardnotes/syncing-server-js/commit/9be4c002b755fea057489b6077b297162223aefe))
## [1.95.13](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.12...@standardnotes/syncing-server@1.95.13) (2023-09-13)
### Bug Fixes
* cleanup only for 0 new items ([b1d88b1](https://github.com/standardnotes/syncing-server-js/commit/b1d88b15be78a48224963e337a222fb675ed2692))
* **syncing-server:** add catch up timeout for secondary db ([ff78285](https://github.com/standardnotes/syncing-server-js/commit/ff78285e43db849bdc44caa36f602150562b4d81))
* **syncing-server:** case insensitive integrity check ([d5536f5](https://github.com/standardnotes/syncing-server-js/commit/d5536f54304e2aecd59dbece7650254f7c2101bb))
## [1.95.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.11...@standardnotes/syncing-server@1.95.12) (2023-09-12)
### Bug Fixes
* sync between primary and secondary database with diff ([fab5d18](https://github.com/standardnotes/syncing-server-js/commit/fab5d180645e0a6fa0c9c67205d44f27c8a65c8b))
## [1.95.11](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.10...@standardnotes/syncing-server@1.95.11) (2023-09-12)
### Bug Fixes
* **syncing-server:** binding ([e91a832](https://github.com/standardnotes/syncing-server-js/commit/e91a8321527ac269ba9822ce270184db5bc57099))
## [1.95.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.9...@standardnotes/syncing-server@1.95.10) (2023-09-12) ## [1.95.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.95.9...@standardnotes/syncing-server@1.95.10) (2023-09-12)
### Bug Fixes ### 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 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" echo "[Docker] Unknown command"
;; ;;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/syncing-server", "name": "@standardnotes/syncing-server",
"version": "1.95.10", "version": "1.96.1",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
@@ -666,14 +666,14 @@ export class ContainerConfigLoader {
.bind<SyncItems>(TYPES.Sync_SyncItems) .bind<SyncItems>(TYPES.Sync_SyncItems)
.toConstantValue( .toConstantValue(
new SyncItems( new SyncItems(
container.get(TYPES.Sync_ItemRepositoryResolver), container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
container.get(TYPES.Sync_GetItems), container.get<GetItems>(TYPES.Sync_GetItems),
container.get(TYPES.Sync_SaveItems), container.get<SaveItems>(TYPES.Sync_SaveItems),
container.get(TYPES.Sync_GetSharedVaults), container.get<GetSharedVaults>(TYPES.Sync_GetSharedVaults),
container.get(TYPES.Sync_GetSharedVaultInvitesSentToUser), container.get<GetSharedVaultInvitesSentToUser>(TYPES.Sync_GetSharedVaultInvitesSentToUser),
container.get(TYPES.Sync_GetMessagesSentToUser), container.get<GetMessagesSentToUser>(TYPES.Sync_GetMessagesSentToUser),
container.get(TYPES.Sync_GetUserNotifications), container.get<GetUserNotifications>(TYPES.Sync_GetUserNotifications),
container.get(TYPES.Sync_Timer), container.get<Logger>(TYPES.Sync_Logger),
), ),
) )
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => { container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
@@ -952,6 +952,7 @@ export class ContainerConfigLoader {
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Sync_TransitionStatusUpdatedEventHandler) .bind<TransitionStatusUpdatedEventHandler>(TYPES.Sync_TransitionStatusUpdatedEventHandler)
.toConstantValue( .toConstantValue(
new TransitionStatusUpdatedEventHandler( new TransitionStatusUpdatedEventHandler(
container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
container.get<TransitionItemsFromPrimaryToSecondaryDatabaseForUser>( container.get<TransitionItemsFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser, TYPES.Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser,
), ),
@@ -1089,9 +1090,6 @@ export class ContainerConfigLoader {
container.get<SyncItems>(TYPES.Sync_SyncItems), container.get<SyncItems>(TYPES.Sync_SyncItems),
container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity), container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
container.get<GetItem>(TYPES.Sync_GetItem), container.get<GetItem>(TYPES.Sync_GetItem),
container.get<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Sync_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
),
container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper), container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver), container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer), container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
@@ -172,7 +172,8 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: { createTransitionStatusUpdatedEvent(dto: {
userUuid: string userUuid: string
transitionType: 'items' | 'revisions' transitionType: 'items' | 'revisions'
status: 'STARTED' | 'FAILED' | 'FINISHED' transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent { }): TransitionStatusUpdatedEvent {
return { return {
type: 'TRANSITION_STATUS_UPDATED', type: 'TRANSITION_STATUS_UPDATED',
@@ -52,7 +52,8 @@ export interface DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: { createTransitionStatusUpdatedEvent(dto: {
userUuid: string userUuid: string
transitionType: 'items' | 'revisions' transitionType: 'items' | 'revisions'
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED' | 'VERIFIED'
}): TransitionStatusUpdatedEvent }): TransitionStatusUpdatedEvent
createEmailRequestedEvent(dto: { createEmailRequestedEvent(dto: {
userEmail: string userEmail: string
@@ -18,6 +18,7 @@ export class TransitionRequestedEventHandler implements DomainEventHandlerInterf
const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({ const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
transitionTimestamp: event.payload.timestamp,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -6,9 +6,11 @@ import {
import { Logger } from 'winston' import { Logger } from 'winston'
import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser' import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface { export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
constructor( constructor(
private primaryItemRepository: ItemRepositoryInterface,
private transitionItemsFromPrimaryToSecondaryDatabaseForUser: TransitionItemsFromPrimaryToSecondaryDatabaseForUser, private transitionItemsFromPrimaryToSecondaryDatabaseForUser: TransitionItemsFromPrimaryToSecondaryDatabaseForUser,
private domainEventPublisher: DomainEventPublisherInterface, private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface, private domainEventFactory: DomainEventFactoryInterface,
@@ -16,12 +18,34 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
) {} ) {}
async handle(event: TransitionStatusUpdatedEvent): Promise<void> { async handle(event: TransitionStatusUpdatedEvent): Promise<void> {
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'items') { if (event.payload.transitionType !== 'items') {
return
}
this.logger.info(
`Received transition status updated event to ${event.payload.status} for user ${event.payload.userUuid}`,
)
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,
}),
)
return
}
await this.domainEventPublisher.publish( await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({ this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
status: 'IN_PROGRESS', status: 'IN_PROGRESS',
transitionType: 'items', transitionType: 'items',
transitionTimestamp: event.payload.transitionTimestamp,
}), }),
) )
@@ -37,6 +61,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
status: 'FAILED', status: 'FAILED',
transitionType: 'items', transitionType: 'items',
transitionTimestamp: event.payload.transitionTimestamp,
}), }),
) )
@@ -48,8 +73,28 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
userUuid: event.payload.userUuid, userUuid: event.payload.userUuid,
status: 'FINISHED', status: 'FINISHED',
transitionType: 'items', 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,
})
}
} }
} }
private async isAlreadyMigrated(userUuid: string): Promise<boolean> {
const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid })
if (totalItemsCountForUser > 0) {
this.logger.info(`User ${userUuid} has ${totalItemsCountForUser} items in primary database.`)
}
return totalItemsCountForUser === 0
}
} }
@@ -22,7 +22,7 @@ describe('CheckIntegrity', () => {
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
}, },
{ {
uuid: '2-3-4', uuid: '2-3-4-a',
updated_at_timestamp: 2, updated_at_timestamp: 2,
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
}, },
@@ -56,7 +56,7 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 1, updated_at_timestamp: 1,
}, },
{ {
uuid: '2-3-4', uuid: '2-3-4-A',
updated_at_timestamp: 2, updated_at_timestamp: 2,
}, },
{ {
@@ -82,7 +82,7 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 1, updated_at_timestamp: 1,
}, },
{ {
uuid: '2-3-4', uuid: '2-3-4-A',
updated_at_timestamp: 1, updated_at_timestamp: 1,
}, },
{ {
@@ -98,7 +98,7 @@ describe('CheckIntegrity', () => {
}) })
expect(result.getValue()).toEqual([ expect(result.getValue()).toEqual([
{ {
uuid: '2-3-4', uuid: '2-3-4-A',
updated_at_timestamp: 2, updated_at_timestamp: 2,
}, },
]) ])
@@ -113,7 +113,7 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 1, updated_at_timestamp: 1,
}, },
{ {
uuid: '2-3-4', uuid: '2-3-4-A',
updated_at_timestamp: 2, updated_at_timestamp: 2,
}, },
{ {
@@ -140,7 +140,7 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 1, updated_at_timestamp: 1,
}, },
{ {
uuid: '2-3-4', uuid: '2-3-4-A',
updated_at_timestamp: 2, updated_at_timestamp: 2,
}, },
{ {
@@ -20,15 +20,17 @@ export class CheckIntegrity implements UseCaseInterface<IntegrityPayload[]> {
const serverItemIntegrityPayloadsMap = new Map<string, ExtendedIntegrityPayload>() const serverItemIntegrityPayloadsMap = new Map<string, ExtendedIntegrityPayload>()
for (const serverItemIntegrityPayload of serverItemIntegrityPayloads) { for (const serverItemIntegrityPayload of serverItemIntegrityPayloads) {
serverItemIntegrityPayloadsMap.set(serverItemIntegrityPayload.uuid, serverItemIntegrityPayload) serverItemIntegrityPayloadsMap.set(serverItemIntegrityPayload.uuid.toLowerCase(), serverItemIntegrityPayload)
} }
const clientItemIntegrityPayloadsMap = new Map<string, number>() const clientItemIntegrityPayloadsMap = new Map<string, number>()
const caseInsensitiveUuidsMap = new Map<string, string>()
for (const clientItemIntegrityPayload of dto.integrityPayloads) { for (const clientItemIntegrityPayload of dto.integrityPayloads) {
clientItemIntegrityPayloadsMap.set( clientItemIntegrityPayloadsMap.set(
clientItemIntegrityPayload.uuid, clientItemIntegrityPayload.uuid.toLowerCase(),
clientItemIntegrityPayload.updated_at_timestamp, clientItemIntegrityPayload.updated_at_timestamp,
) )
caseInsensitiveUuidsMap.set(clientItemIntegrityPayload.uuid.toLowerCase(), clientItemIntegrityPayload.uuid)
} }
const mismatches: IntegrityPayload[] = [] const mismatches: IntegrityPayload[] = []
@@ -58,7 +60,7 @@ export class CheckIntegrity implements UseCaseInterface<IntegrityPayload[]> {
serverItemIntegrityPayload.content_type !== ContentType.TYPES.ItemsKey serverItemIntegrityPayload.content_type !== ContentType.TYPES.ItemsKey
) { ) {
mismatches.unshift({ mismatches.unshift({
uuid: serverItemIntegrityPayloadUuid, uuid: caseInsensitiveUuidsMap.get(serverItemIntegrityPayloadUuid) as string,
updated_at_timestamp: serverItemIntegrityPayloadUpdatedAtTimestamp, updated_at_timestamp: serverItemIntegrityPayloadUpdatedAtTimestamp,
}) })
} }
@@ -298,7 +298,7 @@ describe('UpdateExistingItem', () => {
expect(itemRepository.save).toHaveBeenCalled() 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 useCase = createUseCase()
const result = await useCase.execute({ const result = await useCase.execute({
@@ -308,13 +308,59 @@ describe('UpdateExistingItem', () => {
...itemHash1.props, ...itemHash1.props,
created_at: undefined, created_at: undefined,
created_at_timestamp: undefined, created_at_timestamp: undefined,
updated_at_timestamp: 123,
}).getValue(), }).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000', sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000', performingUserUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser], 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 () => { 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 { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser' import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface' import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
import { ItemHash } from '../../../Item/ItemHash'
export class UpdateExistingItem implements UseCaseInterface<Item> { export class UpdateExistingItem implements UseCaseInterface<Item> {
constructor( constructor(
@@ -115,17 +116,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
) )
const updatedAtDate = this.timer.convertMicrosecondsToDate(updatedAtTimestamp) const updatedAtDate = this.timer.convertMicrosecondsToDate(updatedAtTimestamp)
let createdAtTimestamp: number const { createdAtDate, createdAtTimestamp } = this.determineCreatedAt(dto.itemHash)
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 datesOrError = Dates.create(createdAtDate, updatedAtDate) const datesOrError = Dates.create(createdAtDate, updatedAtDate)
if (datesOrError.isFailed()) { if (datesOrError.isFailed()) {
@@ -221,6 +212,29 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
return Result.ok(dto.existingItem) 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( private async addNotificationsAndPublishEvents(
userUuid: Uuid, userUuid: Uuid,
sharedVaultOperation: SharedVaultOperationOnItem | null, sharedVaultOperation: SharedVaultOperationOnItem | null,
@@ -1,417 +0,0 @@
import { Logger } from 'winston'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from './TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
import { Item } from '../../../Item/Item'
import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
describe('TransitionItemsFromPrimaryToSecondaryDatabaseForUser', () => {
let primaryItemRepository: ItemRepositoryInterface
let secondaryItemRepository: ItemRepositoryInterface | null
let logger: Logger
let primaryItem1: Item
let primaryItem2: Item
let secondaryItem1: Item
let secondaryItem2: Item
let timer: TimerInterface
const createUseCase = () =>
new TransitionItemsFromPrimaryToSecondaryDatabaseForUser(
primaryItemRepository,
secondaryItemRepository,
timer,
logger,
1,
)
beforeEach(() => {
primaryItem1 = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id=1',
content: 'content-1',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key-1',
authHash: 'auth-hash-1',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
primaryItem2 = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id=2',
content: 'content-2',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key-2',
authHash: 'auth-hash-2',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: true,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
).getValue()
secondaryItem1 = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id=1',
content: 'content-1',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key-1',
authHash: 'auth-hash-1',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
secondaryItem2 = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id=2',
content: 'content-2',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key-2',
authHash: 'auth-hash-2',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: true,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
).getValue()
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
primaryItemRepository.countAll = jest.fn().mockResolvedValue(2)
primaryItemRepository.findAll = jest
.fn()
.mockResolvedValueOnce([primaryItem1])
.mockResolvedValueOnce([primaryItem2])
.mockResolvedValueOnce([primaryItem1])
.mockResolvedValueOnce([primaryItem2])
primaryItemRepository.deleteByUserUuid = jest.fn().mockResolvedValue(undefined)
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
secondaryItemRepository.save = jest.fn().mockResolvedValue(undefined)
secondaryItemRepository.deleteByUserUuid = jest.fn().mockResolvedValue(undefined)
secondaryItemRepository.countAll = jest.fn().mockReturnValueOnce(0).mockReturnValueOnce(2)
secondaryItemRepository.findByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryItem1)
.mockResolvedValueOnce(secondaryItem2)
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
logger.info = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.sleep = jest.fn()
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
timer.convertMicrosecondsToTimeStructure = jest.fn().mockReturnValue({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
})
})
describe('successfull transition', () => {
it('should transition items from primary to secondary database', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(3)
expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(primaryItemRepository.findAll).toHaveBeenCalledTimes(4)
expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(1, {
userUuid: '00000000-0000-0000-0000-000000000000',
limit: 1,
offset: 0,
})
expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(2, {
userUuid: '00000000-0000-0000-0000-000000000000',
limit: 1,
offset: 1,
})
expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(3, {
userUuid: '00000000-0000-0000-0000-000000000000',
limit: 1,
offset: 0,
})
expect(primaryItemRepository.findAll).toHaveBeenNthCalledWith(4, {
userUuid: '00000000-0000-0000-0000-000000000000',
limit: 1,
offset: 1,
})
expect((secondaryItemRepository as ItemRepositoryInterface).save).toHaveBeenCalledTimes(2)
expect((secondaryItemRepository as ItemRepositoryInterface).save).toHaveBeenCalledWith(primaryItem1)
expect((secondaryItemRepository as ItemRepositoryInterface).save).toHaveBeenCalledWith(primaryItem2)
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).not.toHaveBeenCalled()
expect(primaryItemRepository.deleteByUserUuid).toHaveBeenCalledTimes(1)
})
it('should log an error if deleting items from primary database fails', async () => {
primaryItemRepository.deleteByUserUuid = jest.fn().mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up primary database items for user 00000000-0000-0000-0000-000000000000: error',
)
})
})
describe('failed transition', () => {
it('should remove items from secondary database if integrity check fails', async () => {
const secondaryItem2WithDifferentContent = Item.create({
...secondaryItem2.props,
content: 'different-content',
}).getValue()
;(secondaryItemRepository as ItemRepositoryInterface).findByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryItem1)
.mockResolvedValueOnce(secondaryItem2WithDifferentContent)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
})
it('should remove items from secondary database if migrating items fails', async () => {
primaryItemRepository.findAll = jest
.fn()
.mockResolvedValueOnce([primaryItem1])
.mockRejectedValueOnce(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('error')
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
})
it('should log an error if deleting items from secondary database fails upon migration failure', async () => {
primaryItemRepository.findAll = jest
.fn()
.mockResolvedValueOnce([primaryItem1])
.mockRejectedValueOnce(new Error('error'))
;(secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid = jest
.fn()
.mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up secondary database items for user 00000000-0000-0000-0000-000000000000: error',
)
})
it('should log an error if deleting items from secondary database fails upon integrity check failure', async () => {
const secondaryItem2WithDifferentContent = Item.create({
...secondaryItem2.props,
content: 'different-content',
}).getValue()
;(secondaryItemRepository as ItemRepositoryInterface).findByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryItem1)
.mockResolvedValueOnce(secondaryItem2WithDifferentContent)
;(secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid = jest
.fn()
.mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith(
'Failed to clean up secondary database items for user 00000000-0000-0000-0000-000000000000: error',
)
})
it('should not perform the transition if secondary item repository is not set', async () => {
secondaryItemRepository = null
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Secondary item repository is not set')
expect(primaryItemRepository.countAll).not.toHaveBeenCalled()
expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
})
it('should not perform the transition if the user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid-uuid',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid-uuid')
expect(primaryItemRepository.countAll).not.toHaveBeenCalled()
expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
})
it('should fail integrity check if the item count is not the same in both databases', async () => {
;(secondaryItemRepository as ItemRepositoryInterface).countAll = jest
.fn()
.mockResolvedValueOnce(0)
.mockResolvedValueOnce(1)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Total items count for user 00000000-0000-0000-0000-000000000000 in primary database (2) does not match total items count in secondary database (1)',
)
expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(3)
expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect((secondaryItemRepository as ItemRepositoryInterface).countAll).toHaveBeenCalledTimes(2)
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if one item is not found in the secondary database', async () => {
;(secondaryItemRepository as ItemRepositoryInterface).findByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryItem1)
.mockResolvedValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Item 00000000-0000-0000-0000-000000000001 not found in secondary database')
expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(3)
expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect((secondaryItemRepository as ItemRepositoryInterface).countAll).toHaveBeenCalledTimes(2)
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if an error is thrown during integrity check between primary and secondary database', async () => {
;(secondaryItemRepository as ItemRepositoryInterface).countAll = jest
.fn()
.mockReturnValueOnce(0)
.mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('error')
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).toHaveBeenCalledTimes(1)
})
})
it('should not migrate items if there are no items in the primary database', async () => {
primaryItemRepository.countAll = jest.fn().mockResolvedValue(0)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(primaryItemRepository.countAll).toHaveBeenCalledTimes(1)
expect(primaryItemRepository.countAll).toHaveBeenCalledWith({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
expect((secondaryItemRepository as ItemRepositoryInterface).save).not.toHaveBeenCalled()
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).not.toHaveBeenCalled()
})
it('should not migrate items if there are items in the secondary database', async () => {
;(secondaryItemRepository as ItemRepositoryInterface).countAll = jest.fn().mockResolvedValue(1)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(primaryItemRepository.findAll).not.toHaveBeenCalled()
expect((secondaryItemRepository as ItemRepositoryInterface).save).not.toHaveBeenCalled()
expect(primaryItemRepository.deleteByUserUuid).not.toHaveBeenCalled()
expect((secondaryItemRepository as ItemRepositoryInterface).deleteByUserUuid).not.toHaveBeenCalled()
})
})
@@ -1,3 +1,4 @@
/* istanbul ignore file */
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston' import { Logger } from 'winston'
@@ -5,6 +6,7 @@ import { TransitionItemsFromPrimaryToSecondaryDatabaseForUserDTO } from './Trans
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface' import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { ItemQuery } from '../../../Item/ItemQuery' import { ItemQuery } from '../../../Item/ItemQuery'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { Item } from '../../../Item/Item'
export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> { export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
constructor( constructor(
@@ -28,25 +30,48 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
} }
const userUuid = userUuidOrError.getValue() const userUuid = userUuidOrError.getValue()
if (await this.isAlreadyMigrated(userUuid)) { let newItemsInSecondaryCount = 0
this.logger.info(`Items for user ${userUuid.value} are already migrated`) let updatedItemsInSecondary: Item[] = []
return Result.ok()
}
if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) { if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
return Result.fail(`Items for user ${userUuid.value} already exist in secondary database`) 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 (newItemsInSecondary.length > 0) {
this.logger.info(
`Found ${newItemsInSecondary.length} new items in secondary database for user ${userUuid.value}`,
)
}
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 migrationTimeStart = this.timer.getTimestampInMicroseconds()
const migrationResult = await this.migrateItemsForUser(userUuid) const migrationResult = await this.migrateItemsForUser(userUuid, updatedItemsInSecondary)
if (migrationResult.isFailed()) { if (migrationResult.isFailed()) {
const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository) if (newItemsInSecondaryCount === 0 && updatedItemsInSecondaryCount === 0) {
if (cleanupResult.isFailed()) { const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
this.logger.error( if (cleanupResult.isFailed()) {
`Failed to clean up secondary database items for user ${userUuid.value}: ${cleanupResult.getError()}`, this.logger.error(
) `Failed to clean up secondary database items for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
}
} }
return Result.fail(migrationResult.getError()) return Result.fail(migrationResult.getError())
@@ -54,13 +79,19 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
await this.allowForSecondaryDatabaseToCatchUp() await this.allowForSecondaryDatabaseToCatchUp()
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid) const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid,
newItemsInSecondaryCount,
updatedItemsInSecondary,
)
if (integrityCheckResult.isFailed()) { if (integrityCheckResult.isFailed()) {
const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository) if (newItemsInSecondaryCount === 0 && updatedItemsInSecondaryCount === 0) {
if (cleanupResult.isFailed()) { const cleanupResult = await this.deleteItemsForUser(userUuid, this.secondaryItemRepository)
this.logger.error( if (cleanupResult.isFailed()) {
`Failed to clean up secondary database items for user ${userUuid.value}: ${cleanupResult.getError()}`, this.logger.error(
) `Failed to clean up secondary database items for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
}
} }
return Result.fail(integrityCheckResult.getError()) return Result.fail(integrityCheckResult.getError())
@@ -90,13 +121,12 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
userUuid: userUuid.value, userUuid: userUuid.value,
}) })
return totalItemsCountForUser > 0 const hasAlreadyDataInSecondaryDatabase = totalItemsCountForUser > 0
} if (hasAlreadyDataInSecondaryDatabase) {
this.logger.info(`User ${userUuid.value} has already ${totalItemsCountForUser} items in secondary database`)
}
private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> { return hasAlreadyDataInSecondaryDatabase
const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
return totalItemsCountForUser === 0
} }
private async allowForSecondaryDatabaseToCatchUp(): Promise<void> { private async allowForSecondaryDatabaseToCatchUp(): Promise<void> {
@@ -104,7 +134,68 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
await this.timer.sleep(twoSecondsInMilliseconds) await this.timer.sleep(twoSecondsInMilliseconds)
} }
private async migrateItemsForUser(userUuid: Uuid): Promise<Result<void>> { 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 { itemInPrimary, newerItemInSecondary } = await this.checkIfItemExistsInPrimaryDatabase(item)
if (itemInPrimary !== null) {
alreadyExistingInPrimary.push(item)
continue
}
if (newerItemInSecondary !== null) {
updatedInSecondary.push(newerItemInSecondary)
continue
}
if (itemInPrimary === null && newerItemInSecondary === null) {
newItemsInSecondary.push(item)
continue
}
}
return {
alreadyExistingInPrimary,
newItemsInSecondary,
updatedInSecondary,
}
}
private async checkIfItemExistsInPrimaryDatabase(
item: Item,
): Promise<{ itemInPrimary: Item | null; newerItemInSecondary: Item | null }> {
const itemInPrimary = await this.primaryItemRepository.findByUuid(item.uuid)
if (itemInPrimary === null) {
return { itemInPrimary: null, newerItemInSecondary: null }
}
if (!item.isIdenticalTo(itemInPrimary)) {
this.logger.error(
`Revision ${item.id.toString()} is not identical in primary and secondary database. Revision in secondary database: ${JSON.stringify(
item,
)}, revision in primary database: ${JSON.stringify(itemInPrimary)}`,
)
return {
itemInPrimary: null,
newerItemInSecondary: item.props.timestamps.updatedAt > itemInPrimary.props.timestamps.updatedAt ? item : null,
}
}
return { itemInPrimary: itemInPrimary, newerItemInSecondary: null }
}
private async migrateItemsForUser(userUuid: Uuid, updatedItemsInSecondary: Item[]): Promise<Result<void>> {
try { try {
const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value }) const totalItemsCountForUser = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
const totalPages = Math.ceil(totalItemsCountForUser / this.pageSize) const totalPages = Math.ceil(totalItemsCountForUser / this.pageSize)
@@ -118,6 +209,11 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
const items = await this.primaryItemRepository.findAll(query) const items = await this.primaryItemRepository.findAll(query)
for (const item of items) { 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) await (this.secondaryItemRepository as ItemRepositoryInterface).save(item)
} }
} }
@@ -138,7 +234,11 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
} }
} }
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid: Uuid): Promise<Result<boolean>> { private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid: Uuid,
newItemsInSecondaryCount: number,
updatedItemsInSecondary: Item[],
): Promise<Result<boolean>> {
try { try {
const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({ userUuid: userUuid.value }) const totalItemsCountForUserInPrimary = await this.primaryItemRepository.countAll({ userUuid: userUuid.value })
const totalItemsCountForUserInSecondary = await ( const totalItemsCountForUserInSecondary = await (
@@ -147,9 +247,9 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
userUuid: userUuid.value, userUuid: userUuid.value,
}) })
if (totalItemsCountForUserInPrimary !== totalItemsCountForUserInSecondary) { if (totalItemsCountForUserInPrimary + newItemsInSecondaryCount !== totalItemsCountForUserInSecondary) {
return Result.fail( return Result.fail(
`Total items count for user ${userUuid.value} in primary database (${totalItemsCountForUserInPrimary}) does not match total items count in secondary database (${totalItemsCountForUserInSecondary})`, `Total items count for user ${userUuid.value} in primary database (${totalItemsCountForUserInPrimary} + ${newItemsInSecondaryCount}) does not match total items count in secondary database (${totalItemsCountForUserInSecondary})`,
) )
} }
@@ -169,6 +269,13 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
return Result.fail(`Item ${item.uuid.value} not found in secondary database`) 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)) { if (!item.isIdenticalTo(itemInSecondary)) {
return Result.fail( return Result.fail(
`Item ${ `Item ${
@@ -23,6 +23,7 @@ describe('TriggerTransitionFromPrimaryToSecondaryDatabaseForUser', () => {
await useCase.execute({ await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000', userUuid: '00000000-0000-0000-0000-000000000000',
transitionTimestamp: 123,
}) })
expect(domainEventPubliser.publish).toHaveBeenCalled() expect(domainEventPubliser.publish).toHaveBeenCalled()
@@ -15,6 +15,7 @@ export class TriggerTransitionFromPrimaryToSecondaryDatabaseForUser implements U
userUuid: dto.userUuid, userUuid: dto.userUuid,
status: 'STARTED', status: 'STARTED',
transitionType: 'items', transitionType: 'items',
transitionTimestamp: dto.transitionTimestamp,
}) })
await this.domainEventPubliser.publish(event) await this.domainEventPubliser.publish(event)
@@ -1,3 +1,4 @@
export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO { export interface TriggerTransitionFromPrimaryToSecondaryDatabaseForUserDTO {
userUuid: string userUuid: string
transitionTimestamp: number
} }
@@ -11,7 +11,6 @@ import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { BaseItemsController } from './Base/BaseItemsController' import { BaseItemsController } from './Base/BaseItemsController'
import { MapperInterface } from '@standardnotes/domain-core' import { MapperInterface } from '@standardnotes/domain-core'
import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation' import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
@controller('/items', TYPES.Sync_AuthMiddleware) @controller('/items', TYPES.Sync_AuthMiddleware)
export class AnnotatedItemsController extends BaseItemsController { export class AnnotatedItemsController extends BaseItemsController {
@@ -19,20 +18,11 @@ export class AnnotatedItemsController extends BaseItemsController {
@inject(TYPES.Sync_SyncItems) override syncItems: SyncItems, @inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
@inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity, @inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
@inject(TYPES.Sync_GetItem) override getItem: GetItem, @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_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
@inject(TYPES.Sync_SyncResponseFactoryResolver) @inject(TYPES.Sync_SyncResponseFactoryResolver)
override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface, override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
) { ) {
super( super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver)
syncItems,
checkIntegrity,
getItem,
triggerTransitionFromPrimaryToSecondaryDatabaseForUser,
itemHttpMapper,
syncResponseFactoryResolver,
)
} }
@httpPost('/sync') @httpPost('/sync')
@@ -45,11 +35,6 @@ export class AnnotatedItemsController extends BaseItemsController {
return super.checkItemsIntegrity(request, response) return super.checkItemsIntegrity(request, response)
} }
@httpPost('/transition')
override async transition(request: Request, response: Response): Promise<results.JsonResult> {
return super.transition(request, response)
}
@httpGet('/:uuid') @httpGet('/:uuid')
override async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> { override async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
return super.getSingleItem(request, response) return super.getSingleItem(request, response)
@@ -12,14 +12,12 @@ import { ApiVersion } from '../../../Domain/Api/ApiVersion'
import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems' import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
import { ItemHash } from '../../../Domain/Item/ItemHash' import { ItemHash } from '../../../Domain/Item/ItemHash'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../../../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
export class BaseItemsController extends BaseHttpController { export class BaseItemsController extends BaseHttpController {
constructor( constructor(
protected syncItems: SyncItems, protected syncItems: SyncItems,
protected checkIntegrity: CheckIntegrity, protected checkIntegrity: CheckIntegrity,
protected getItem: GetItem, protected getItem: GetItem,
protected triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>, protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface, protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
private controllerContainer?: ControllerContainerInterface, 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.sync', this.sync.bind(this))
this.controllerContainer.register('sync.items.check_integrity', this.checkItemsIntegrity.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.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> { async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.getItem.execute({ const result = await this.getItem.execute({
userUuid: response.locals.user.uuid, userUuid: response.locals.user.uuid,
@@ -18,7 +18,6 @@ export class MongoDBItem {
declare content: string | null declare content: string | null
@Column() @Column()
@Index('index_items_on_content_type')
declare contentType: string | null declare contentType: string | null
@Column() @Column()
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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.10.40](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.39...@standardnotes/websockets-server@1.10.40) (2023-09-12) ## [1.10.40](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.39...@standardnotes/websockets-server@1.10.40) (2023-09-12)
**Note:** Version bump only for package @standardnotes/websockets-server **Note:** Version bump only for package @standardnotes/websockets-server
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/websockets-server", "name": "@standardnotes/websockets-server",
"version": "1.10.40", "version": "1.10.42",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+52 -75
View File
@@ -2659,10 +2659,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@hexagon/base64@npm:^1.1.25": "@hexagon/base64@npm:^1.1.27":
version: 1.1.26 version: 1.1.27
resolution: "@hexagon/base64@npm:1.1.26" resolution: "@hexagon/base64@npm:1.1.27"
checksum: e42582ed12465bffaf96307c9d5b7dfd36166ec4dc41a1838b9a560c90c9d136d006099a205e1684fb0dc18002cc5af51a49dd7b81a7c4b86798372d6ee26af3 checksum: 899fffaf54b291e1df997bf33dbf6e068fcfbd83155adc114e14bcb9c1e36c5f820dfaaee3d0c2409f7e84efa4352f51655eac8bec4c2432fca443bf179bce8d
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3793,7 +3793,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@peculiar/asn1-android@npm:^2.3.3": "@peculiar/asn1-android@npm:^2.3.6":
version: 2.3.6 version: 2.3.6
resolution: "@peculiar/asn1-android@npm:2.3.6" resolution: "@peculiar/asn1-android@npm:2.3.6"
dependencies: dependencies:
@@ -3804,7 +3804,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@peculiar/asn1-ecc@npm:^2.3.4": "@peculiar/asn1-ecc@npm:^2.3.6":
version: 2.3.6 version: 2.3.6
resolution: "@peculiar/asn1-ecc@npm:2.3.6" resolution: "@peculiar/asn1-ecc@npm:2.3.6"
dependencies: dependencies:
@@ -3816,7 +3816,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@peculiar/asn1-rsa@npm:^2.3.4": "@peculiar/asn1-rsa@npm:^2.3.6":
version: 2.3.6 version: 2.3.6
resolution: "@peculiar/asn1-rsa@npm:2.3.6" resolution: "@peculiar/asn1-rsa@npm:2.3.6"
dependencies: dependencies:
@@ -3828,7 +3828,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.3.6
resolution: "@peculiar/asn1-schema@npm:2.3.6" resolution: "@peculiar/asn1-schema@npm:2.3.6"
dependencies: dependencies:
@@ -3839,7 +3839,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.3.6
resolution: "@peculiar/asn1-x509@npm:2.3.6" resolution: "@peculiar/asn1-x509@npm:2.3.6"
dependencies: dependencies:
@@ -3987,41 +3987,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@simplewebauthn/iso-webcrypto@npm:^7.2.0": "@simplewebauthn/server@npm:^8.1.1":
version: 7.2.0 version: 8.1.1
resolution: "@simplewebauthn/iso-webcrypto@npm:7.2.0" resolution: "@simplewebauthn/server@npm:8.1.1"
dependencies: dependencies:
"@simplewebauthn/typescript-types": "npm:*" "@hexagon/base64": "npm:^1.1.27"
"@types/node": "npm:^18.11.9" "@peculiar/asn1-android": "npm:^2.3.6"
checksum: b57899d0ada391507ce8f4601328ed62df5d09f75f6e91b018278631270a96f37ceab95f9e824c9555dd05820e5a99ac386ed067db7902d37bdb6d995fbd7eaf "@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 languageName: node
linkType: hard linkType: hard
"@simplewebauthn/server@npm:^7.2.0": "@simplewebauthn/typescript-types@npm:^8.0.0":
version: 7.2.0 version: 8.0.0
resolution: "@simplewebauthn/server@npm:7.2.0" resolution: "@simplewebauthn/typescript-types@npm:8.0.0"
dependencies: checksum: 21e0b13268f237d7cd6ecdc6cdceb884ddcc85e18a3554b65d10da4f502056aefa810db94f504beaad96b4ac0c7861a022954dcc6aa8721d4215571c2c3dcdf5
"@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
languageName: node languageName: node
linkType: hard linkType: hard
@@ -4744,8 +4730,8 @@ __metadata:
"@cbor-extract/cbor-extract-linux-arm64": "npm:^2.1.1" "@cbor-extract/cbor-extract-linux-arm64": "npm:^2.1.1"
"@cbor-extract/cbor-extract-linux-x64": "npm:^2.1.1" "@cbor-extract/cbor-extract-linux-x64": "npm:^2.1.1"
"@newrelic/winston-enricher": "npm:^4.0.1" "@newrelic/winston-enricher": "npm:^4.0.1"
"@simplewebauthn/server": "npm:^7.2.0" "@simplewebauthn/server": "npm:^8.1.1"
"@simplewebauthn/typescript-types": "npm:^7.0.0" "@simplewebauthn/typescript-types": "npm:^8.0.0"
"@standardnotes/api": "npm:^1.26.26" "@standardnotes/api": "npm:^1.26.26"
"@standardnotes/common": "workspace:*" "@standardnotes/common": "workspace:*"
"@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-core": "workspace:^"
@@ -5589,15 +5575,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@types/dotenv@npm:^8.2.0":
version: 8.2.0 version: 8.2.0
resolution: "@types/dotenv@npm:8.2.0" resolution: "@types/dotenv@npm:8.2.0"
@@ -5792,13 +5769,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@types/newrelic@npm:^9.14.0":
version: 9.14.0 version: 9.14.0
resolution: "@types/newrelic@npm:9.14.0" resolution: "@types/newrelic@npm:9.14.0"
@@ -5820,13 +5790,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@types/node@npm:^20.5.7":
version: 20.5.7 version: 20.5.7
resolution: "@types/node@npm:20.5.7" resolution: "@types/node@npm:20.5.7"
@@ -7208,15 +7171,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cbor-x@npm:^1.4.1": "cbor-x@npm:^1.5.2":
version: 1.5.3 version: 1.5.4
resolution: "cbor-x@npm:1.5.3" resolution: "cbor-x@npm:1.5.4"
dependencies: dependencies:
cbor-extract: "npm:^2.1.1" cbor-extract: "npm:^2.1.1"
dependenciesMeta: dependenciesMeta:
cbor-extract: cbor-extract:
optional: true optional: true
checksum: d4df85b33969826f4c96a4b4a8fbe03132fb0817fba876f16d41ad6d1a7d2668ec04c923f313220506029cc2b5ab212901ba24b4594d0115e0f527ef31506fbf checksum: 742aea498abfe004a7ff4db2a1c0e00d9e9c1d89db4ad9aa94a9b886cd2ce10a133f20e32788c83696eef368e18c2b5bc82e4b1480c5af91937816a5630989d6
languageName: node languageName: node
linkType: hard linkType: hard
@@ -7848,12 +7811,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cross-fetch@npm:^3.1.5": "cross-fetch@npm:^4.0.0":
version: 3.1.6 version: 4.0.0
resolution: "cross-fetch@npm:3.1.6" resolution: "cross-fetch@npm:4.0.0"
dependencies: dependencies:
node-fetch: "npm:^2.6.11" node-fetch: "npm:^2.6.12"
checksum: a8989fca821cae97520976d00f85ce7c3ab8af7e00cc06c94fd94c49ada6847f4cdeabca8e0ebd4aa6c7343f70bea7e0c64d5910b846aab218136a450585aa61 checksum: 30e86b703a455baca17b7f2088fdd88b71193b39e7cb61f3385511dc6064b7741c816329c0abff8a74d306969455c8797131d056518a981fd4d2424ecd4ab451
languageName: node languageName: node
linkType: hard linkType: hard
@@ -12007,7 +11970,21 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.6.11
resolution: "node-fetch@npm:2.6.11" resolution: "node-fetch@npm:2.6.11"
dependencies: dependencies: