Compare commits

...

65 Commits

Author SHA1 Message Date
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
standardci
4f95bbee3f chore(release): publish new version
- @standardnotes/analytics@2.26.11
 - @standardnotes/api-gateway@1.74.8
 - @standardnotes/auth-server@1.141.7
 - @standardnotes/domain-events-infra@1.12.27
 - @standardnotes/domain-events@2.125.3
 - @standardnotes/event-store@1.11.39
 - @standardnotes/files-server@1.22.18
 - @standardnotes/home-server@1.15.43
 - @standardnotes/revisions-server@1.33.13
 - @standardnotes/scheduler-server@1.20.43
 - @standardnotes/syncing-server@1.95.10
 - @standardnotes/websockets-server@1.10.40
2023-09-12 13:37:37 +00:00
Karol Sójko
b9c9f74d0c fix(syncing-server): log syncing errors 2023-09-12 15:03:11 +02:00
Karol Sójko
e535cd504c fix: retry failed revision transitions 2023-09-12 14:45:17 +02:00
standardci
db0360860a chore(release): publish new version
- @standardnotes/home-server@1.15.42
 - @standardnotes/revisions-server@1.33.12
 - @standardnotes/syncing-server@1.95.9
2023-09-12 10:57:29 +00:00
Karol Sójko
aa2b5f3b74 chore: add logs for failed item dumps 2023-09-12 12:13:55 +02:00
standardci
6241661e27 chore(release): publish new version
- @standardnotes/home-server@1.15.41
 - @standardnotes/syncing-server@1.95.8
2023-09-12 09:38:19 +00:00
Karol Sójko
25047bf46d fix(syncing-server): allow fetching shared vault users for members (#821) 2023-09-12 11:00:55 +02:00
standardci
a1820ed212 chore(release): publish new version
- @standardnotes/analytics@2.26.10
 - @standardnotes/api-gateway@1.74.7
 - @standardnotes/auth-server@1.141.6
 - @standardnotes/domain-core@1.28.1
 - @standardnotes/event-store@1.11.38
 - @standardnotes/files-server@1.22.17
 - @standardnotes/home-server@1.15.40
 - @standardnotes/revisions-server@1.33.11
 - @standardnotes/scheduler-server@1.20.42
 - @standardnotes/settings@1.21.31
 - @standardnotes/syncing-server@1.95.7
 - @standardnotes/websockets-server@1.10.39
2023-09-12 08:55:11 +00:00
Karol Sójko
0a1d1624e8 fix: comparing uuids 2023-09-12 10:21:42 +02:00
standardci
7367de6832 chore(release): publish new version
- @standardnotes/analytics@2.26.9
 - @standardnotes/api-gateway@1.74.6
 - @standardnotes/auth-server@1.141.5
 - @standardnotes/domain-events-infra@1.12.26
 - @standardnotes/domain-events@2.125.2
 - @standardnotes/event-store@1.11.37
 - @standardnotes/files-server@1.22.16
 - @standardnotes/home-server@1.15.39
 - @standardnotes/revisions-server@1.33.10
 - @standardnotes/scheduler-server@1.20.41
 - @standardnotes/syncing-server@1.95.6
 - @standardnotes/websockets-server@1.10.38
2023-09-12 07:40:20 +00:00
Karol Sójko
f0abfe89fc fix: domain event values 2023-09-12 08:59:28 +02:00
standardci
d1244d165a chore(release): publish new version
- @standardnotes/analytics@2.26.8
 - @standardnotes/api-gateway@1.74.5
 - @standardnotes/auth-server@1.141.4
 - @standardnotes/domain-events-infra@1.12.25
 - @standardnotes/domain-events@2.125.1
 - @standardnotes/event-store@1.11.36
 - @standardnotes/files-server@1.22.15
 - @standardnotes/home-server@1.15.38
 - @standardnotes/revisions-server@1.33.9
 - @standardnotes/scheduler-server@1.20.40
 - @standardnotes/security@1.13.1
 - @standardnotes/syncing-server@1.95.5
 - @standardnotes/websockets-server@1.10.37
2023-09-12 06:58:11 +00:00
Karol Sójko
106d8f9192 fix: adjust transitions to not create revisions during ongoing revisions transition 2023-09-12 08:22:50 +02:00
standardci
1d86ba8fcb chore(release): publish new version
- @standardnotes/auth-server@1.141.3
 - @standardnotes/home-server@1.15.37
 - @standardnotes/revisions-server@1.33.8
 - @standardnotes/syncing-server@1.95.4
2023-09-12 05:31:57 +00:00
Karol Sójko
f20a947f8a fix: transition adjustments 2023-09-12 06:52:34 +02:00
standardci
19b9de05ae chore(release): publish new version
- @standardnotes/home-server@1.15.36
 - @standardnotes/syncing-server@1.95.3
2023-09-11 19:06:39 +00:00
Karol Sójko
1d751c0fbe fix: debug sync block 2023-09-11 19:23:09 +02:00
standardci
fa2564e164 chore(release): publish new version
- @standardnotes/auth-server@1.141.2
 - @standardnotes/home-server@1.15.35
 - @standardnotes/revisions-server@1.33.7
 - @standardnotes/syncing-server@1.95.2
2023-09-11 15:18:55 +00:00
Karol Sójko
330bff0124 fix(auth): remove transitioning upon sign out (#819) 2023-09-11 16:42:11 +02:00
standardci
ca57c8e7b5 chore(release): publish new version
- @standardnotes/auth-server@1.141.1
 - @standardnotes/home-server@1.15.34
 - @standardnotes/revisions-server@1.33.6
 - @standardnotes/syncing-server@1.95.1
2023-09-11 13:00:46 +00:00
Karol Sójko
a82b9a0c8a fix: disable running migrations in worker mode of a given service 2023-09-11 14:27:52 +02:00
standardci
ea7e9d73c4 chore(release): publish new version
- @standardnotes/api-gateway@1.74.4
 - @standardnotes/auth-server@1.141.0
 - @standardnotes/home-server@1.15.33
 - @standardnotes/revisions-server@1.33.5
2023-09-11 11:23:06 +00:00
Karol Sójko
117b7b4b99 fix(revisions): removing queries 2023-09-11 12:42:40 +02:00
Karol Sójko
b4bf11d9da fix(revisions): legacy table syncing and select for metadata 2023-09-11 12:23:11 +02:00
Karol Sójko
0306e10469 fix: db debug level on e2e 2023-09-11 12:03:15 +02:00
Karol Sójko
0ab47013f2 fix(api-gateway): awaiting for other services to start 2023-09-11 12:00:43 +02:00
Karol Sójko
836883b82d fix(revisions): rename table only if exists 2023-09-11 11:56:57 +02:00
Karol Sójko
ed671be9c5 fix(revisions): conflict with table naming 2023-09-11 11:52:39 +02:00
Karol Sójko
9676a2586c fix(revisions): add item_uuid to revisions metadata http representation 2023-09-11 11:39:42 +02:00
Karol Sójko
e95ba61c7f feat(auth): add procedure to transition users created between dates (#816) 2023-09-11 10:40:40 +02:00
standardci
a0718aea26 chore(release): publish new version
- @standardnotes/home-server@1.15.32
 - @standardnotes/revisions-server@1.33.4
2023-09-11 07:17:47 +00:00
Karol Sójko
156fa7a618 fix(revisions): add shared vault uuid to revision metadata http representation 2023-09-11 08:44:18 +02:00
standardci
8d006ece30 chore(release): publish new version
- @standardnotes/analytics@2.26.7
 - @standardnotes/api-gateway@1.74.3
 - @standardnotes/auth-server@1.140.0
 - @standardnotes/domain-events-infra@1.12.24
 - @standardnotes/domain-events@2.125.0
 - @standardnotes/event-store@1.11.35
 - @standardnotes/files-server@1.22.14
 - @standardnotes/home-server@1.15.31
 - @standardnotes/revisions-server@1.33.3
 - @standardnotes/scheduler-server@1.20.39
 - @standardnotes/syncing-server@1.95.0
 - @standardnotes/websockets-server@1.10.36
2023-09-08 14:54:00 +00:00
Karol Sójko
037c994040 chore: add run name for e2e test suite 2023-09-08 15:21:26 +02:00
Karol Sójko
a164ba291d fix(auth): specs 2023-09-08 13:41:37 +02:00
Karol Sójko
610fba2601 fix(auth): transition users only when transition mode enabled 2023-09-08 13:34:54 +02:00
Karol Sójko
398338c8f8 feat: transition users after sign out 2023-09-08 12:51:44 +02:00
standardci
34be157d8e chore(release): publish new version
- @standardnotes/home-server@1.15.30
 - @standardnotes/revisions-server@1.33.2
2023-09-08 10:08:48 +00:00
Karol Sójko
76372fe357 fix(revisions): persistence mapper (#814) 2023-09-08 11:29:03 +02:00
standardci
46879c336b chore(release): publish new version
- @standardnotes/auth-server@1.139.0
 - @standardnotes/home-server@1.15.29
 - @standardnotes/syncing-server@1.94.0
2023-09-08 07:53:10 +00:00
Karol Sójko
aef9e936bd feat(auth): add vaults user role into database 2023-09-08 09:19:52 +02:00
Karol Sójko
8cb92d9678 feat(syncing-server): add procedure to trigger transition for specific user 2023-09-08 09:14:07 +02:00
Karol Sójko
52db89de81 chore: disable fail fast on e2e 2023-09-07 19:03:38 +02:00
standardci
c5af8dfc05 chore(release): publish new version
- @standardnotes/home-server@1.15.28
 - @standardnotes/revisions-server@1.33.1
 - @standardnotes/syncing-server@1.93.1
2023-09-07 16:23:44 +00:00
Karol Sójko
27ad8e6959 fix(revisions): waiting for syncing-server on self-hosted setup 2023-09-07 17:44:53 +02:00
Karol Sójko
c4a1502f70 fix(syncing-server): invalidating cache for user removed from shared vault (#812) 2023-09-07 15:41:34 +02:00
standardci
d1d6c753c4 chore(release): publish new version
- @standardnotes/analytics@2.26.6
 - @standardnotes/api-gateway@1.74.2
 - @standardnotes/auth-server@1.138.2
 - @standardnotes/domain-events-infra@1.12.23
 - @standardnotes/domain-events@2.124.0
 - @standardnotes/event-store@1.11.34
 - @standardnotes/files-server@1.22.13
 - @standardnotes/home-server@1.15.27
 - @standardnotes/revisions-server@1.33.0
 - @standardnotes/scheduler-server@1.20.38
 - @standardnotes/syncing-server@1.93.0
 - @standardnotes/websockets-server@1.10.35
2023-09-07 10:46:30 +00:00
Karol Sójko
3bd63f7674 feat: add removing revisions from shared vaults (#811) 2023-09-07 12:02:38 +02:00
144 changed files with 2438 additions and 1253 deletions

1
.github/ci.env vendored
View File

@@ -4,6 +4,7 @@ DB_USERNAME=std_notes_user
DB_PASSWORD=changeme123
DB_DATABASE=standard_notes_db
DB_PORT=3306
DB_DEBUG_LEVEL=all
DB_SQLITE_DATABASE_PATH=standard_notes_db
REDIS_PORT=6379
REDIS_HOST=cache

View File

@@ -21,7 +21,7 @@ jobs:
e2e:
name: (Self Hosting) E2E Test Suite
strategy:
fail-fast: true
fail-fast: false
matrix:
secondary_db_enabled: [true, false]
transition_mode_enabled: [true, false]
@@ -70,7 +70,7 @@ jobs:
e2e-home-server:
name: (Home Server) E2E Test Suite
strategy:
fail-fast: true
fail-fast: false
matrix:
db_type: [mysql, sqlite]
cache_type: [redis, memory]
@@ -141,6 +141,7 @@ jobs:
echo "DB_USERNAME=standardnotes" >> packages/home-server/.env
echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env
echo "DB_TYPE=${{ matrix.db_type }}" >> packages/home-server/.env
echo "DB_DEBUG_LEVEL=all" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:6379" >> packages/home-server/.env
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
echo "SECONDARY_DB_ENABLED=${{ matrix.secondary_db_enabled }}" >> packages/home-server/.env

View File

@@ -1,5 +1,7 @@
name: E2E Test Suite On Self Hosted Server
run-name: E2E Test Suite against ${{ inputs.ref_name }} by ${{ inputs.author }}
on:
schedule:
- cron: '0 */12 * * *'
@@ -9,6 +11,14 @@ on:
type: string
default: latest
description: The Docker image tag used for SNJS container
author:
type: string
default: unknown
description: The author that triggered the workflow
ref_name:
type: string
default: unknown
description: The ref name from which the workflow was triggered
jobs:
e2e:

View File

@@ -53,7 +53,7 @@ services:
image: mysql:8
container_name: db-ci
env_file: .github/ci.env
expose:
ports:
- 3306
restart: unless-stopped
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
@@ -66,7 +66,7 @@ services:
secondary_db:
image: mongo:5.0
container_name: secondary_db-ci
expose:
ports:
- 27017
restart: unless-stopped
volumes:
@@ -83,7 +83,7 @@ services:
container_name: cache-ci
volumes:
- ./data/redis/:/data
expose:
ports:
- 6379
restart: unless-stopped
networks:

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @standardnotes/analytics
## [2.26.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.9...@standardnotes/analytics@2.26.10) (2023-09-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.26.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.8...@standardnotes/analytics@2.26.9) (2023-09-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.26.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.7...@standardnotes/analytics@2.26.8) (2023-09-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.26.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.6...@standardnotes/analytics@2.26.7) (2023-09-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.26.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.5...@standardnotes/analytics@2.26.6) (2023-09-07)
**Note:** Version bump only for package @standardnotes/analytics
## [2.26.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.4...@standardnotes/analytics@2.26.5) (2023-09-07)
**Note:** Version bump only for package @standardnotes/analytics

View File

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

View File

@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.74.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.6...@standardnotes/api-gateway@1.74.7) (2023-09-12)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.74.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.5...@standardnotes/api-gateway@1.74.6) (2023-09-12)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.74.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.4...@standardnotes/api-gateway@1.74.5) (2023-09-12)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.74.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.3...@standardnotes/api-gateway@1.74.4) (2023-09-11)
### Bug Fixes
* **api-gateway:** awaiting for other services to start ([0ab4701](https://github.com/standardnotes/api-gateway/commit/0ab47013f210dca7aa404966798011947fb5c362))
## [1.74.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.2...@standardnotes/api-gateway@1.74.3) (2023-09-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.74.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.1...@standardnotes/api-gateway@1.74.2) (2023-09-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.74.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.0...@standardnotes/api-gateway@1.74.1) (2023-09-07)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

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

View File

@@ -2,5 +2,8 @@
set -euo pipefail
sh supervisor/wait-for.sh localhost $AUTH_SERVER_PORT
sh supervisor/wait-for.sh localhost $FILES_SERVER_PORT
sh supervisor/wait-for.sh localhost $REVISIONS_SERVER_PORT
sh supervisor/wait-for.sh localhost $SYNCING_SERVER_PORT
node docker/entrypoint-server.js

View File

@@ -3,6 +3,87 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
* retry failed revision transitions ([e535cd5](https://github.com/standardnotes/server/commit/e535cd504cf1929539ff7faf13e9c1fdd2b7bfd1))
## [1.141.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.5...@standardnotes/auth-server@1.141.6) (2023-09-12)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.141.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.4...@standardnotes/auth-server@1.141.5) (2023-09-12)
### Bug Fixes
* domain event values ([f0abfe8](https://github.com/standardnotes/server/commit/f0abfe89fca0049c47131389683efe2f5aff23f8))
## [1.141.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.3...@standardnotes/auth-server@1.141.4) (2023-09-12)
### Bug Fixes
* adjust transitions to not create revisions during ongoing revisions transition ([106d8f9](https://github.com/standardnotes/server/commit/106d8f9192f630794ca4ddc2c4503f2c6cd196e7))
## [1.141.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.2...@standardnotes/auth-server@1.141.3) (2023-09-12)
### Bug Fixes
* transition adjustments ([f20a947](https://github.com/standardnotes/server/commit/f20a947f8a555c074d8dc1543c7a8bf61d1d887e))
## [1.141.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.1...@standardnotes/auth-server@1.141.2) (2023-09-11)
### Bug Fixes
* **auth:** remove transitioning upon sign out ([#819](https://github.com/standardnotes/server/issues/819)) ([330bff0](https://github.com/standardnotes/server/commit/330bff0124f5f49c3441304d166ea43c21fea7bc))
## [1.141.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.141.0...@standardnotes/auth-server@1.141.1) (2023-09-11)
### Bug Fixes
* disable running migrations in worker mode of a given service ([a82b9a0](https://github.com/standardnotes/server/commit/a82b9a0c8a023ba8a450ff9e34bcd62f928fcab3))
# [1.141.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.140.0...@standardnotes/auth-server@1.141.0) (2023-09-11)
### Features
* **auth:** add procedure to transition users created between dates ([#816](https://github.com/standardnotes/server/issues/816)) ([e95ba61](https://github.com/standardnotes/server/commit/e95ba61c7f769736698ebbc38179d6dc05a8cc5e))
# [1.140.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.139.0...@standardnotes/auth-server@1.140.0) (2023-09-08)
### Bug Fixes
* **auth:** specs ([a164ba2](https://github.com/standardnotes/server/commit/a164ba291d4893b5a0bb4b39a8795d654212a1a6))
* **auth:** transition users only when transition mode enabled ([610fba2](https://github.com/standardnotes/server/commit/610fba260150d0757020e01ac0c8967feeadd4d6))
### Features
* transition users after sign out ([398338c](https://github.com/standardnotes/server/commit/398338c8f81b12406c7ab3bf1654e60b94d7cfd0))
# [1.139.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.138.2...@standardnotes/auth-server@1.139.0) (2023-09-08)
### Features
* **auth:** add vaults user role into database ([aef9e93](https://github.com/standardnotes/server/commit/aef9e936bdbd1f4ccc32658d3d892e7675ec0e0e))
## [1.138.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.138.1...@standardnotes/auth-server@1.138.2) (2023-09-07)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.138.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.138.0...@standardnotes/auth-server@1.138.1) (2023-09-07)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -75,7 +75,7 @@ const requestBackups = async (
})
}
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
dayjs.extend(utc)

View File

@@ -18,7 +18,7 @@ const cleanup = async (
await cleanupExpiredSessions.execute({ date })
}
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()

View File

@@ -8,7 +8,7 @@ import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { PersistStatistics } from '../src/Domain/UseCase/PersistStatistics/PersistStatistics'
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()

View File

@@ -0,0 +1,97 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { TransitionStatusRepositoryInterface } from '../src/Domain/Transition/TransitionStatusRepositoryInterface'
const inputArgs = process.argv.slice(2)
const startDateString = inputArgs[0]
const endDateString = inputArgs[1]
const requestTransition = async (
userRepository: UserRepositoryInterface,
transitionStatusRepository: TransitionStatusRepositoryInterface,
logger: Logger,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const startDate = new Date(startDateString)
const endDate = new Date(endDateString)
const users = await userRepository.findAllCreatedBetween(startDate, endDate)
logger.info(`Found ${users.length} users created between ${startDateString} and ${endDateString}`)
let usersTriggered = 0
for (const user of users) {
const transitionRequestedEvent = domainEventFactory.createTransitionRequestedEvent({
userUuid: user.uuid,
type: 'items',
})
usersTriggered += 1
await domainEventPublisher.publish(transitionRequestedEvent)
}
logger.info(
`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')
void container.load().then((container) => {
dayjs.extend(utc)
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Auth_Logger)
logger.info(`Starting transition request for users created between ${startDateString} and ${endDateString}`)
const userRepository: UserRepositoryInterface = container.get(TYPES.Auth_UserRepository)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.Auth_DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.Auth_DomainEventPublisher)
const transitionStatusRepository: TransitionStatusRepositoryInterface = container.get(
TYPES.Auth_TransitionStatusRepository,
)
Promise.resolve(
requestTransition(userRepository, transitionStatusRepository, logger, domainEventFactory, domainEventPublisher),
)
.then(() => {
logger.info(`Finished transition request for users created between ${startDateString} and ${endDateString}`)
process.exit(0)
})
.catch((error) => {
logger.error(
`Error while requesting transition for users created between ${startDateString} and ${endDateString}: ${error}`,
)
process.exit(1)
})
})

View File

@@ -63,7 +63,7 @@ const requestBackups = async (
return
}
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
dayjs.extend(utc)

View File

@@ -9,7 +9,7 @@ import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-eve
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
dayjs.extend(utc)

View File

@@ -0,0 +1,11 @@
'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

View File

@@ -55,6 +55,13 @@ case "$COMMAND" in
node docker/entrypoint-backup.js one_drive daily
;;
'transition' )
START_DATE=$1 && shift 1
END_DATE=$1 && shift 1
echo "[Docker] Starting Transition..."
node docker/entrypoint-transition.js $START_DATE $END_DATE
;;
* )
echo "[Docker] Unknown command"
;;

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddVaultsUser1694157482134 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("35669f45-a2d8-4172-bdab-b7b3d42044ce", "VAULTS_USER", 1)',
)
}
public async down(): Promise<void> {
return
}
}

View File

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

View File

@@ -274,6 +274,8 @@ import { UserAddedToSharedVaultEventHandler } from '../Domain/Handler/UserAddedT
import { UserRemovedFromSharedVaultEventHandler } from '../Domain/Handler/UserRemovedFromSharedVaultEventHandler'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
async load(configuration?: {
controllerConatiner?: ControllerContainerInterface
directCallDomainEventPublisher?: DirectCallDomainEventPublisher
@@ -310,7 +312,7 @@ export class ContainerConfigLoader {
}
container.bind<winston.Logger>(TYPES.Auth_Logger).toConstantValue(logger)
const appDataSource = new AppDataSource(env)
const appDataSource = new AppDataSource({ env, runMigrations: this.mode === 'server' })
await appDataSource.initialize()
logger.debug('Database initialized')
@@ -941,6 +943,7 @@ export class ContainerConfigLoader {
new UpdateTransitionStatus(
container.get<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository),
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container

View File

@@ -23,7 +23,12 @@ import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
export class AppDataSource {
private _dataSource: DataSource | undefined
constructor(private env: Env) {}
constructor(
private configuration: {
env: Env
runMigrations: boolean
},
) {}
getRepository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): Repository<Entity> {
if (!this._dataSource) {
@@ -38,12 +43,12 @@ export class AppDataSource {
}
get dataSource(): DataSource {
this.env.load()
this.configuration.env.load()
const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql'
const isConfiguredForMySQL = this.configuration.env.get('DB_TYPE') === 'mysql'
const maxQueryExecutionTime = this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
const maxQueryExecutionTime = this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const commonDataSourceOptions = {
@@ -68,28 +73,28 @@ export class AppDataSource {
TypeORMSharedVaultUser,
],
migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,
logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
migrationsRun: this.configuration.runMigrations,
logging: <LoggerOptions>this.configuration.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
}
if (isConfiguredForMySQL) {
const inReplicaMode = this.env.get('DB_REPLICA_HOST', true) ? true : false
const inReplicaMode = this.configuration.env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: this.env.get('DB_HOST'),
port: parseInt(this.env.get('DB_PORT')),
username: this.env.get('DB_USERNAME'),
password: this.env.get('DB_PASSWORD'),
database: this.env.get('DB_DATABASE'),
host: this.configuration.env.get('DB_HOST'),
port: parseInt(this.configuration.env.get('DB_PORT')),
username: this.configuration.env.get('DB_USERNAME'),
password: this.configuration.env.get('DB_PASSWORD'),
database: this.configuration.env.get('DB_DATABASE'),
},
slaves: [
{
host: this.env.get('DB_REPLICA_HOST', true),
port: parseInt(this.env.get('DB_PORT')),
username: this.env.get('DB_USERNAME'),
password: this.env.get('DB_PASSWORD'),
database: this.env.get('DB_DATABASE'),
host: this.configuration.env.get('DB_REPLICA_HOST', true),
port: parseInt(this.configuration.env.get('DB_PORT')),
username: this.configuration.env.get('DB_USERNAME'),
password: this.configuration.env.get('DB_PASSWORD'),
database: this.configuration.env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
@@ -103,11 +108,11 @@ export class AppDataSource {
supportBigNumbers: true,
bigNumberStrings: false,
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : this.env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(this.env.get('DB_PORT')),
username: inReplicaMode ? undefined : this.env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : this.env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : this.env.get('DB_DATABASE'),
host: inReplicaMode ? undefined : this.configuration.env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(this.configuration.env.get('DB_PORT')),
username: inReplicaMode ? undefined : this.configuration.env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : this.configuration.env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : this.configuration.env.get('DB_DATABASE'),
}
this._dataSource = new DataSource(mySQLDataSourceOptions)
@@ -115,7 +120,7 @@ export class AppDataSource {
const sqliteDataSourceOptions: SqliteConnectionOptions = {
...commonDataSourceOptions,
type: 'sqlite',
database: this.env.get('DB_SQLITE_DATABASE_PATH'),
database: this.configuration.env.get('DB_SQLITE_DATABASE_PATH'),
enableWAL: true,
busyErrorRetry: 2000,
}

View File

@@ -4,4 +4,4 @@ import { Env } from './Env'
const env: Env = new Env()
env.load()
export const MigrationsDataSource = new AppDataSource(env).dataSource
export const MigrationsDataSource = new AppDataSource({ env, runMigrations: true }).dataSource

View File

@@ -20,6 +20,7 @@ import {
StatisticPersistenceRequestedEvent,
SessionCreatedEvent,
SessionRefreshedEvent,
TransitionRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
@@ -32,6 +33,24 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
createTransitionRequestedEvent(dto: { userUuid: string; type: 'items' | 'revisions' }): TransitionRequestedEvent {
return {
type: 'TRANSITION_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Auth,
},
payload: {
timestamp: this.timer.getTimestampInMicroseconds(),
...dto,
},
}
}
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent {
return {
type: 'SESSION_CREATED',

View File

@@ -18,6 +18,7 @@ import {
StatisticPersistenceRequestedEvent,
SessionCreatedEvent,
SessionRefreshedEvent,
TransitionRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
@@ -89,4 +90,5 @@ export interface DomainEventFactoryInterface {
}): StatisticPersistenceRequestedEvent
createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent
createSessionRefreshedEvent(dto: { userUuid: string }): SessionRefreshedEvent
createTransitionRequestedEvent(dto: { userUuid: string; type: 'items' | 'revisions' }): TransitionRequestedEvent
}

View File

@@ -13,6 +13,7 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
status: event.payload.status,
userUuid: event.payload.userUuid,
transitionType: event.payload.transitionType,
transitionTimestamp: event.payload.transitionTimestamp,
})
if (result.isFailed()) {

View File

@@ -1,5 +1,15 @@
export interface TransitionStatusRepositoryInterface {
updateStatus(userUuid: string, transitionType: 'items' | 'revisions', status: 'STARTED' | 'FAILED'): Promise<void>
updateStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED',
): Promise<void>
removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void>
getStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<'STARTED' | 'FAILED' | null>
getStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null>
getStatuses(
transitionType: 'items' | 'revisions',
): Promise<Array<{ userUuid: string; status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' }>>
}

View File

@@ -113,13 +113,14 @@ describe('CreateCrossServiceToken', () => {
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
ongoing_revisions_transition: false,
},
60,
)
})
it('should create a cross service token for user that has an ongoing transaction', async () => {
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('STARTED')
transitionStatusRepository.getStatus = jest.fn().mockReturnValue('IN_PROGRESS')
await createUseCase().execute({
user,
@@ -148,6 +149,7 @@ describe('CreateCrossServiceToken', () => {
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: true,
ongoing_revisions_transition: true,
},
60,
)
@@ -177,6 +179,7 @@ describe('CreateCrossServiceToken', () => {
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
ongoing_revisions_transition: false,
},
60,
)
@@ -206,6 +209,7 @@ describe('CreateCrossServiceToken', () => {
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_transition: false,
ongoing_revisions_transition: false,
},
60,
)
@@ -261,6 +265,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
ongoing_revisions_transition: false,
ongoing_transition: false,
},
60,

View File

@@ -48,6 +48,7 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
}
const transitionStatus = await this.transitionStatusRepository.getStatus(user.uuid, 'items')
const revisionsTransitionStatus = await this.transitionStatusRepository.getStatus(user.uuid, 'revisions')
const roles = await user.roles
@@ -59,7 +60,9 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
user: this.projectUser(user),
roles: this.projectRoles(roles),
shared_vault_owner_context: undefined,
ongoing_transition: transitionStatus === 'STARTED',
ongoing_transition: transitionStatus === 'IN_PROGRESS',
ongoing_revisions_transition:
revisionsTransitionStatus === 'STARTED' || revisionsTransitionStatus === 'IN_PROGRESS',
belongs_to_shared_vaults: sharedVaultAssociations.map((association) => ({
shared_vault_uuid: association.props.sharedVaultUuid.value,
permission: association.props.permission.value,

View File

@@ -4,13 +4,17 @@ import { GetTransitionStatusDTO } from './GetTransitionStatusDTO'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
export class GetTransitionStatus implements UseCaseInterface<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'> {
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' | 'FINISHED' | 'FAILED'>> {
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())

View File

@@ -3,17 +3,36 @@ import { RoleName, Uuid } from '@standardnotes/domain-core'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatus } from './UpdateTransitionStatus'
import { Logger } from 'winston'
describe('UpdateTransitionStatus', () => {
let transitionStatusRepository: TransitionStatusRepositoryInterface
let roleService: RoleServiceInterface
let logger: Logger
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService)
const createUseCase = () => new UpdateTransitionStatus(transitionStatusRepository, roleService, logger)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
transitionStatusRepository.removeStatus = jest.fn()
transitionStatusRepository.updateStatus = jest.fn()
transitionStatusRepository.getStatuses = jest.fn().mockResolvedValue([
{
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'STARTED',
},
{
userUuid: '00000000-0000-0000-0000-000000000001',
status: 'IN_PROGRESS',
},
{
userUuid: '00000000-0000-0000-0000-000000000002',
status: 'FAILED',
},
])
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addRoleToUser = jest.fn()
@@ -26,6 +45,7 @@ describe('UpdateTransitionStatus', () => {
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'FINISHED',
transitionType: 'items',
transitionTimestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
@@ -39,6 +59,24 @@ describe('UpdateTransitionStatus', () => {
)
})
it('should remove transition status', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'FINISHED',
transitionType: 'revisions',
transitionTimestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(transitionStatusRepository.removeStatus).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000',
'revisions',
)
expect(roleService.addRoleToUser).not.toHaveBeenCalled()
})
it('should update transition status', async () => {
const useCase = createUseCase()
@@ -46,6 +84,7 @@ describe('UpdateTransitionStatus', () => {
userUuid: '00000000-0000-0000-0000-000000000000',
status: 'STARTED',
transitionType: 'items',
transitionTimestamp: 123,
})
expect(result.isFailed()).toBeFalsy()
@@ -63,6 +102,7 @@ describe('UpdateTransitionStatus', () => {
userUuid: 'invalid',
status: 'STARTED',
transitionType: 'items',
transitionTimestamp: 123,
})
expect(result.isFailed()).toBeTruthy()

View File

@@ -2,11 +2,13 @@ import { Result, RoleName, UseCaseInterface, Uuid } from '@standardnotes/domain-
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
import { UpdateTransitionStatusDTO } from './UpdateTransitionStatusDTO'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { Logger } from 'winston'
export class UpdateTransitionStatus implements UseCaseInterface<void> {
constructor(
private transitionStatusRepository: TransitionStatusRepositoryInterface,
private roleService: RoleServiceInterface,
private logger: Logger,
) {}
async execute(dto: UpdateTransitionStatusDTO): Promise<Result<void>> {
@@ -16,15 +18,35 @@ export class UpdateTransitionStatus implements UseCaseInterface<void> {
}
const userUuid = userUuidOrError.getValue()
if (dto.status === 'FINISHED') {
await this.transitionStatusRepository.removeStatus(dto.userUuid, dto.transitionType)
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
if (dto.status !== 'FINISHED') {
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
return Result.ok()
}
await this.transitionStatusRepository.updateStatus(dto.userUuid, dto.transitionType, dto.status)
await this.transitionStatusRepository.removeStatus(dto.userUuid, dto.transitionType)
if (dto.transitionType === 'items') {
await this.roleService.addRoleToUser(userUuid, RoleName.create(RoleName.NAMES.TransitionUser).getValue())
}
const itemStatuses = await this.transitionStatusRepository.getStatuses('items')
const itemsStartedStatusesCount = itemStatuses.filter((status) => status.status === 'STARTED').length
const itemsInProgressStatusesCount = itemStatuses.filter((status) => status.status === 'IN_PROGRESS').length
const itemsFailedStatusesCount = itemStatuses.filter((status) => status.status === 'FAILED').length
this.logger.info(
`[TRANSITION ${dto.transitionTimestamp}] Items transition statuses: ${itemsStartedStatusesCount} started, ${itemsInProgressStatusesCount} in progress, ${itemsFailedStatusesCount} failed`,
)
const revisionStatuses = await this.transitionStatusRepository.getStatuses('revisions')
const revisionsStartedStatusesCount = revisionStatuses.filter((status) => status.status === 'STARTED').length
const revisionsInProgressStatusesCount = revisionStatuses.filter((status) => status.status === 'IN_PROGRESS').length
const revisionsFailedStatusesCount = revisionStatuses.filter((status) => status.status === 'FAILED').length
this.logger.info(
`[TRANSITION ${dto.transitionTimestamp}] Revisions transition statuses: ${revisionsStartedStatusesCount} started, ${revisionsInProgressStatusesCount} in progress, ${revisionsFailedStatusesCount} failed`,
)
return Result.ok()
}

View File

@@ -1,5 +1,6 @@
export interface UpdateTransitionStatusDTO {
userUuid: string
transitionType: 'items' | 'revisions'
status: 'STARTED' | 'FINISHED' | 'FAILED'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'
}

View File

@@ -8,6 +8,7 @@ export interface UserRepositoryInterface {
streamTeam(memberEmail?: Email): Promise<ReadStream>
findOneByUuid(uuid: Uuid): Promise<User | null>
findOneByUsernameOrEmail(usernameOrEmail: Email | Username): Promise<User | null>
findAllCreatedBetween(start: Date, end: Date): Promise<User[]>
save(user: User): Promise<User>
remove(user: User): Promise<User>
}

View File

@@ -4,6 +4,24 @@ export class InMemoryTransitionStatusRepository implements TransitionStatusRepos
private itemStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
private revisionStatuses: Map<string, 'STARTED' | 'FAILED'> = new Map()
async getStatuses(
transitionType: 'items' | 'revisions',
): Promise<{ userUuid: string; status: 'STARTED' | 'FAILED' | 'IN_PROGRESS' }[]> {
const statuses: { userUuid: string; status: 'STARTED' | 'FAILED' | 'IN_PROGRESS' }[] = []
if (transitionType === 'items') {
for (const [userUuid, status] of this.itemStatuses) {
statuses.push({ userUuid, status })
}
} else {
for (const [userUuid, status] of this.revisionStatuses) {
statuses.push({ userUuid, status })
}
}
return statuses
}
async updateStatus(
userUuid: string,
transitionType: 'items' | 'revisions',

View File

@@ -7,21 +7,44 @@ export class RedisTransitionStatusRepository implements TransitionStatusReposito
constructor(private redisClient: IORedis.Redis) {}
async getStatuses(
transitionType: 'items' | 'revisions',
): Promise<{ userUuid: string; status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' }[]> {
const keys = await this.redisClient.keys(`${this.PREFIX}:${transitionType}:*`)
const statuses = await Promise.all(
keys.map(async (key) => {
const userUuid = key.split(':')[2]
const status = (await this.redisClient.get(key)) as 'STARTED' | 'IN_PROGRESS' | 'FAILED'
return { userUuid, status }
}),
)
return statuses
}
async updateStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
status: 'STARTED' | 'FAILED',
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED',
): Promise<void> {
await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status)
if (status === 'IN_PROGRESS') {
await this.redisClient.setex(`${this.PREFIX}:${transitionType}:${userUuid}`, 7200, status)
} else {
await this.redisClient.set(`${this.PREFIX}:${transitionType}:${userUuid}`, status)
}
}
async removeStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<void> {
await this.redisClient.del(`${this.PREFIX}:${transitionType}:${userUuid}`)
}
async getStatus(userUuid: string, transitionType: 'items' | 'revisions'): Promise<'STARTED' | 'FAILED' | null> {
async getStatus(
userUuid: string,
transitionType: 'items' | 'revisions',
): Promise<'STARTED' | 'IN_PROGRESS' | 'FAILED' | null> {
const status = (await this.redisClient.get(`${this.PREFIX}:${transitionType}:${userUuid}`)) as
| 'STARTED'
| 'IN_PROGRESS'
| 'FAILED'
| null

View File

@@ -14,6 +14,13 @@ export class TypeORMUserRepository implements UserRepositoryInterface {
private ormRepository: Repository<User>,
) {}
async findAllCreatedBetween(start: Date, end: Date): Promise<User[]> {
return this.ormRepository
.createQueryBuilder('user')
.where('user.created_at BETWEEN :start AND :end', { start, end })
.getMany()
}
async save(user: User): Promise<User> {
return this.ormRepository.save(user)
}

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.28.0...@standardnotes/domain-core@1.28.1) (2023-09-12)
### Bug Fixes
* comparing uuids ([0a1d162](https://github.com/standardnotes/server/commit/0a1d1624e818000f2e951f29323a88e6e233c755))
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.27.0...@standardnotes/domain-core@1.28.0) (2023-09-07)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.28.0",
"version": "1.28.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -8,6 +8,13 @@ describe('Uuid', () => {
expect(valueOrError.getValue().value).toEqual('84c0f8e8-544a-4c7e-9adf-26209303bc1d')
})
it('should create a value object on upper case', () => {
const valueOrError = Uuid.create('00B57455-B563-4B50-A2AA-B19762102219')
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('00B57455-B563-4B50-A2AA-B19762102219')
})
it('should not create an invalid value object', () => {
const valueOrError = Uuid.create('1-2-3')

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.26](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.25...@standardnotes/domain-events-infra@1.12.26) (2023-09-12)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.25](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.24...@standardnotes/domain-events-infra@1.12.25) (2023-09-12)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.24](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.23...@standardnotes/domain-events-infra@1.12.24) (2023-09-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.23](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.22...@standardnotes/domain-events-infra@1.12.23) (2023-09-07)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.22](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.21...@standardnotes/domain-events-infra@1.12.22) (2023-09-06)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

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

View File

@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
* retry failed revision transitions ([e535cd5](https://github.com/standardnotes/server/commit/e535cd504cf1929539ff7faf13e9c1fdd2b7bfd1))
## [2.125.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.125.1...@standardnotes/domain-events@2.125.2) (2023-09-12)
### Bug Fixes
* domain event values ([f0abfe8](https://github.com/standardnotes/server/commit/f0abfe89fca0049c47131389683efe2f5aff23f8))
## [2.125.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.125.0...@standardnotes/domain-events@2.125.1) (2023-09-12)
**Note:** Version bump only for package @standardnotes/domain-events
# [2.125.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.124.0...@standardnotes/domain-events@2.125.0) (2023-09-08)
### Features
* transition users after sign out ([398338c](https://github.com/standardnotes/server/commit/398338c8f81b12406c7ab3bf1654e60b94d7cfd0))
# [2.124.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.123.0...@standardnotes/domain-events@2.124.0) (2023-09-07)
### Features
* add removing revisions from shared vaults ([#811](https://github.com/standardnotes/server/issues/811)) ([3bd63f7](https://github.com/standardnotes/server/commit/3bd63f767464baf9b9f1ffa52eea9eed4a4e11b5))
# [2.123.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.122.2...@standardnotes/domain-events@2.123.0) (2023-09-06)
### Features

View File

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

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { ItemRemovedFromSharedVaultEventPayload } from './ItemRemovedFromSharedVaultEventPayload'
export interface ItemRemovedFromSharedVaultEvent extends DomainEventInterface {
type: 'ITEM_REMOVED_FROM_SHARED_VAULT'
payload: ItemRemovedFromSharedVaultEventPayload
}

View File

@@ -0,0 +1,6 @@
export interface ItemRemovedFromSharedVaultEventPayload {
userUuid: string
itemUuid: string
sharedVaultUuid: string
roleNames: string[]
}

View File

@@ -0,0 +1,8 @@
import { DomainEventInterface } from './DomainEventInterface'
import { TransitionRequestedEventPayload } from './TransitionRequestedEventPayload'
export interface TransitionRequestedEvent extends DomainEventInterface {
type: 'TRANSITION_REQUESTED'
payload: TransitionRequestedEventPayload
}

View File

@@ -0,0 +1,5 @@
export interface TransitionRequestedEventPayload {
userUuid: string
type: 'items' | 'revisions'
timestamp: number
}

View File

@@ -1,5 +1,6 @@
export interface TransitionStatusUpdatedEventPayload {
userUuid: string
transitionType: 'items' | 'revisions'
status: 'STARTED' | 'FINISHED' | 'FAILED'
transitionTimestamp: number
status: 'STARTED' | 'IN_PROGRESS' | 'FINISHED' | 'FAILED'
}

View File

@@ -32,6 +32,8 @@ export * from './Event/FileUploadedEvent'
export * from './Event/FileUploadedEventPayload'
export * from './Event/ItemDumpedEvent'
export * from './Event/ItemDumpedEventPayload'
export * from './Event/ItemRemovedFromSharedVaultEvent'
export * from './Event/ItemRemovedFromSharedVaultEventPayload'
export * from './Event/ItemRevisionCreationRequestedEvent'
export * from './Event/ItemRevisionCreationRequestedEventPayload'
export * from './Event/ListedAccountCreatedEvent'
@@ -94,6 +96,8 @@ export * from './Event/SubscriptionRevertRequestedEvent'
export * from './Event/SubscriptionRevertRequestedEventPayload'
export * from './Event/SubscriptionSyncRequestedEvent'
export * from './Event/SubscriptionSyncRequestedEventPayload'
export * from './Event/TransitionRequestedEvent'
export * from './Event/TransitionRequestedEventPayload'
export * from './Event/TransitionStatusUpdatedEvent'
export * from './Event/TransitionStatusUpdatedEventPayload'
export * from './Event/UserAddedToSharedVaultEvent'

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.38](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.37...@standardnotes/event-store@1.11.38) (2023-09-12)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.37](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.36...@standardnotes/event-store@1.11.37) (2023-09-12)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.36](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.35...@standardnotes/event-store@1.11.36) (2023-09-12)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.35](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.34...@standardnotes/event-store@1.11.35) (2023-09-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.34](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.33...@standardnotes/event-store@1.11.34) (2023-09-07)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.33](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.32...@standardnotes/event-store@1.11.33) (2023-09-07)
**Note:** Version bump only for package @standardnotes/event-store

View File

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

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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)
**Note:** Version bump only for package @standardnotes/files-server
## [1.22.17](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.16...@standardnotes/files-server@1.22.17) (2023-09-12)
**Note:** Version bump only for package @standardnotes/files-server
## [1.22.16](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.15...@standardnotes/files-server@1.22.16) (2023-09-12)
**Note:** Version bump only for package @standardnotes/files-server
## [1.22.15](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.14...@standardnotes/files-server@1.22.15) (2023-09-12)
**Note:** Version bump only for package @standardnotes/files-server
## [1.22.14](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.13...@standardnotes/files-server@1.22.14) (2023-09-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.22.13](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.12...@standardnotes/files-server@1.22.13) (2023-09-07)
**Note:** Version bump only for package @standardnotes/files-server
## [1.22.12](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.11...@standardnotes/files-server@1.22.12) (2023-09-07)
**Note:** Version bump only for package @standardnotes/files-server

View File

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

View File

@@ -3,6 +3,98 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.42](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.41...@standardnotes/home-server@1.15.42) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.41](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.40...@standardnotes/home-server@1.15.41) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.40](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.39...@standardnotes/home-server@1.15.40) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.39](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.38...@standardnotes/home-server@1.15.39) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.38](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.37...@standardnotes/home-server@1.15.38) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.37](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.36...@standardnotes/home-server@1.15.37) (2023-09-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.36](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.35...@standardnotes/home-server@1.15.36) (2023-09-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.35](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.34...@standardnotes/home-server@1.15.35) (2023-09-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.34](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.33...@standardnotes/home-server@1.15.34) (2023-09-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.33](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.32...@standardnotes/home-server@1.15.33) (2023-09-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.32](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.31...@standardnotes/home-server@1.15.32) (2023-09-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.31](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.30...@standardnotes/home-server@1.15.31) (2023-09-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.30](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.29...@standardnotes/home-server@1.15.30) (2023-09-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.29](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.28...@standardnotes/home-server@1.15.29) (2023-09-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.28](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.27...@standardnotes/home-server@1.15.28) (2023-09-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.27](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.26...@standardnotes/home-server@1.15.27) (2023-09-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.15.26](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.25...@standardnotes/home-server@1.15.26) (2023-09-07)
**Note:** Version bump only for package @standardnotes/home-server

View File

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

View File

@@ -3,6 +3,118 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
* retry failed revision transitions ([e535cd5](https://github.com/standardnotes/server/commit/e535cd504cf1929539ff7faf13e9c1fdd2b7bfd1))
## [1.33.12](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.11...@standardnotes/revisions-server@1.33.12) (2023-09-12)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.33.11](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.10...@standardnotes/revisions-server@1.33.11) (2023-09-12)
### Bug Fixes
* comparing uuids ([0a1d162](https://github.com/standardnotes/server/commit/0a1d1624e818000f2e951f29323a88e6e233c755))
## [1.33.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.9...@standardnotes/revisions-server@1.33.10) (2023-09-12)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.33.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.8...@standardnotes/revisions-server@1.33.9) (2023-09-12)
### Bug Fixes
* adjust transitions to not create revisions during ongoing revisions transition ([106d8f9](https://github.com/standardnotes/server/commit/106d8f9192f630794ca4ddc2c4503f2c6cd196e7))
## [1.33.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.7...@standardnotes/revisions-server@1.33.8) (2023-09-12)
### Bug Fixes
* transition adjustments ([f20a947](https://github.com/standardnotes/server/commit/f20a947f8a555c074d8dc1543c7a8bf61d1d887e))
## [1.33.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.6...@standardnotes/revisions-server@1.33.7) (2023-09-11)
### Bug Fixes
* **auth:** remove transitioning upon sign out ([#819](https://github.com/standardnotes/server/issues/819)) ([330bff0](https://github.com/standardnotes/server/commit/330bff0124f5f49c3441304d166ea43c21fea7bc))
## [1.33.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.5...@standardnotes/revisions-server@1.33.6) (2023-09-11)
### Bug Fixes
* disable running migrations in worker mode of a given service ([a82b9a0](https://github.com/standardnotes/server/commit/a82b9a0c8a023ba8a450ff9e34bcd62f928fcab3))
## [1.33.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.4...@standardnotes/revisions-server@1.33.5) (2023-09-11)
### Bug Fixes
* **revisions:** add item_uuid to revisions metadata http representation ([9676a25](https://github.com/standardnotes/server/commit/9676a2586cabc344f3c244a5a20d4e1d57ac4c35))
* **revisions:** conflict with table naming ([ed671be](https://github.com/standardnotes/server/commit/ed671be9c56eb8d54b8dd52a69d5e30e07203aa4))
* **revisions:** legacy table syncing and select for metadata ([b4bf11d](https://github.com/standardnotes/server/commit/b4bf11d9da44cc1d980b6922f532ff487fe582df))
* **revisions:** removing queries ([117b7b4](https://github.com/standardnotes/server/commit/117b7b4b99e09aebfd081433646ec05c2ebf308f))
* **revisions:** rename table only if exists ([836883b](https://github.com/standardnotes/server/commit/836883b82dd436d0e9b3ae3b70f734ceb508e227))
## [1.33.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.3...@standardnotes/revisions-server@1.33.4) (2023-09-11)
### Bug Fixes
* **revisions:** add shared vault uuid to revision metadata http representation ([156fa7a](https://github.com/standardnotes/server/commit/156fa7a618b9f33a839580019d0398c1b838697d))
## [1.33.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.2...@standardnotes/revisions-server@1.33.3) (2023-09-08)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.33.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.1...@standardnotes/revisions-server@1.33.2) (2023-09-08)
### Bug Fixes
* **revisions:** persistence mapper ([#814](https://github.com/standardnotes/server/issues/814)) ([76372fe](https://github.com/standardnotes/server/commit/76372fe3579a99bf44abfce9667fec134baafedf))
## [1.33.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.33.0...@standardnotes/revisions-server@1.33.1) (2023-09-07)
### Bug Fixes
* **revisions:** waiting for syncing-server on self-hosted setup ([27ad8e6](https://github.com/standardnotes/server/commit/27ad8e6959aeb9c2f11ee0fecba37514bf91dda8))
# [1.33.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.32.0...@standardnotes/revisions-server@1.33.0) (2023-09-07)
### Features
* add removing revisions from shared vaults ([#811](https://github.com/standardnotes/server/issues/811)) ([3bd63f7](https://github.com/standardnotes/server/commit/3bd63f767464baf9b9f1ffa52eea9eed4a4e11b5))
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.31.0...@standardnotes/revisions-server@1.32.0) (2023-09-07)
### Bug Fixes

View File

@@ -7,7 +7,7 @@ import { Env } from '../src/Bootstrap/Env'
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()

View File

@@ -4,19 +4,19 @@ export class init1669113322388 implements MigrationInterface {
name = 'init1669113322388'
public async up(queryRunner: QueryRunner): Promise<void> {
await this.syncSchemaBetweenLegacyRevisions(queryRunner)
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `items_key_id` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `creation_date` date NULL, `created_at` datetime(6) NULL, `updated_at` datetime(6) NULL, INDEX `item_uuid` (`item_uuid`), INDEX `user_uuid` (`user_uuid`), INDEX `creation_date` (`creation_date`), INDEX `created_at` (`created_at`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
'CREATE TABLE `revisions_revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `items_key_id` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `creation_date` date NULL, `created_at` datetime(6) NULL, `updated_at` datetime(6) NULL, INDEX `item_uuid` (`item_uuid`), INDEX `user_uuid` (`user_uuid`), INDEX `creation_date` (`creation_date`), INDEX `created_at` (`created_at`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await this.syncSchemaBetweenLegacyRevisions(queryRunner)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
await queryRunner.query('DROP INDEX `user_uuid` ON `revisions`')
await queryRunner.query('DROP INDEX `item_uuid` ON `revisions`')
await queryRunner.query('DROP TABLE `revisions`')
await queryRunner.query('DROP INDEX `created_at` ON `revisions_revisions`')
await queryRunner.query('DROP INDEX `creation_date` ON `revisions_revisions`')
await queryRunner.query('DROP INDEX `user_uuid` ON `revisions_revisions`')
await queryRunner.query('DROP INDEX `item_uuid` ON `revisions_revisions`')
await queryRunner.query('DROP TABLE `revisions_revisions`')
}
private async syncSchemaBetweenLegacyRevisions(queryRunner: QueryRunner): Promise<void> {
@@ -28,14 +28,10 @@ export class init1669113322388 implements MigrationInterface {
return
}
const revisionsTableHasUserUuidColumnQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = "revisions" AND column_name = "user_uuid"',
await queryRunner.query(
'INSERT INTO `revisions_revisions`(`uuid`, `item_uuid`, `user_uuid`, `content`, `content_type`, `items_key_id`, `enc_item_key`, `auth_hash`, `creation_date`, `created_at`, `updated_at`) SELECT `uuid`, `item_uuid`, NULL, `content`, `content_type`, `items_key_id`, `enc_item_key`, `auth_hash`, `creation_date`, `created_at`, `updated_at` FROM `revisions`',
)
const revisionsTableHasUserUuidColumn = revisionsTableHasUserUuidColumnQueryResult[0].count === 1
if (revisionsTableHasUserUuidColumn) {
return
}
await queryRunner.query('ALTER TABLE `revisions` ADD COLUMN `user_uuid` varchar(36) NULL')
await queryRunner.query('DROP TABLE `revisions`')
}
}

View File

@@ -5,24 +5,24 @@ export class removeDateIndexes1669636497932 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const indexRevisionsOnCreatedAt = await queryRunner.manager.query(
'SHOW INDEX FROM `revisions` where `key_name` = "created_at"',
'SHOW INDEX FROM `revisions_revisions` where `key_name` = "created_at"',
)
const indexRevisionsOnCreatedAtExist = indexRevisionsOnCreatedAt && indexRevisionsOnCreatedAt.length > 0
if (indexRevisionsOnCreatedAtExist) {
await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
await queryRunner.query('DROP INDEX `created_at` ON `revisions_revisions`')
}
const indexRevisionsOnCreationDate = await queryRunner.manager.query(
'SHOW INDEX FROM `revisions` where `key_name` = "creation_date"',
'SHOW INDEX FROM `revisions_revisions` where `key_name` = "creation_date"',
)
const indexRevisionsOnCreationDateAtExist = indexRevisionsOnCreationDate && indexRevisionsOnCreationDate.length > 0
if (indexRevisionsOnCreationDateAtExist) {
await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
await queryRunner.query('DROP INDEX `creation_date` ON `revisions_revisions`')
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE INDEX `creation_date` ON `revisions` (`creation_date`)')
await queryRunner.query('CREATE INDEX `created_at` ON `revisions` (`created_at`)')
await queryRunner.query('CREATE INDEX `creation_date` ON `revisions_revisions` (`creation_date`)')
await queryRunner.query('CREATE INDEX `created_at` ON `revisions_revisions` (`created_at`)')
}
}

View File

@@ -4,10 +4,10 @@ export class makeUserUuidNullable1669735585016 implements MigrationInterface {
name = 'makeUserUuidNullable1669735585016'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions_revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NULL')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NOT NULL')
await queryRunner.query('ALTER TABLE `revisions_revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NOT NULL')
}
}

View File

@@ -2,16 +2,18 @@ import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddSharedVaultInformation1693915383950 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revisions` ADD `edited_by` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions` ADD `shared_vault_uuid` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions` ADD `key_system_identifier` varchar(36) NULL')
await queryRunner.query('CREATE INDEX `index_revisions_on_shared_vault_uuid` ON `revisions` (`shared_vault_uuid`)')
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `edited_by` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `shared_vault_uuid` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `key_system_identifier` varchar(36) NULL')
await queryRunner.query(
'CREATE INDEX `index_revisions_on_shared_vault_uuid` ON `revisions_revisions` (`shared_vault_uuid`)',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `index_revisions_on_shared_vault_uuid` ON `revisions`')
await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `key_system_identifier`')
await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `shared_vault_uuid`')
await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `last_edited_by`')
await queryRunner.query('DROP INDEX `index_revisions_on_shared_vault_uuid` ON `revisions_revisions`')
await queryRunner.query('ALTER TABLE `revisions_revisions` DROP COLUMN `key_system_identifier`')
await queryRunner.query('ALTER TABLE `revisions_revisions` DROP COLUMN `shared_vault_uuid`')
await queryRunner.query('ALTER TABLE `revisions_revisions` DROP COLUMN `last_edited_by`')
}
}

View File

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RenameRevisionsTable1694425333972 implements MigrationInterface {
name = 'RenameRevisionsTable1694425333972'
public async up(queryRunner: QueryRunner): Promise<void> {
const revisionsTableExistsQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "revisions"',
)
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
if (revisionsTableExists) {
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
}
}
public async down(): Promise<void> {
return
}
}

View File

@@ -5,15 +5,15 @@ export class initialBoilerplate1682678053275 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE "revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))',
'CREATE TABLE "revisions_revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))',
)
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ')
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ')
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions_revisions" ("item_uuid") ')
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions_revisions" ("user_uuid") ')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "user_uuid"')
await queryRunner.query('DROP INDEX "item_uuid"')
await queryRunner.query('DROP TABLE "revisions"')
await queryRunner.query('DROP TABLE "revisions_revisions"')
}
}

View File

@@ -10,28 +10,30 @@ export class AddSharedVaultInformation1693915775491 implements MigrationInterfac
'CREATE TABLE "temporary_revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6), "edited_by" varchar(36), "shared_vault_uuid" varchar(36), "key_system_identifier" varchar(36))',
)
await queryRunner.query(
'INSERT INTO "temporary_revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "revisions"',
'INSERT INTO "temporary_revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "revisions_revisions"',
)
await queryRunner.query('DROP TABLE "revisions_revisions"')
await queryRunner.query('ALTER TABLE "temporary_revisions" RENAME TO "revisions_revisions"')
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions_revisions" ("user_uuid") ')
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions_revisions" ("item_uuid") ')
await queryRunner.query(
'CREATE INDEX "index_revisions_on_shared_vault_uuid" ON "revisions_revisions" ("shared_vault_uuid") ',
)
await queryRunner.query('DROP TABLE "revisions"')
await queryRunner.query('ALTER TABLE "temporary_revisions" RENAME TO "revisions"')
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ')
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ')
await queryRunner.query('CREATE INDEX "index_revisions_on_shared_vault_uuid" ON "revisions" ("shared_vault_uuid") ')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "index_revisions_on_shared_vault_uuid"')
await queryRunner.query('DROP INDEX "item_uuid"')
await queryRunner.query('DROP INDEX "user_uuid"')
await queryRunner.query('ALTER TABLE "revisions" RENAME TO "temporary_revisions"')
await queryRunner.query('ALTER TABLE "revisions_revisions" RENAME TO "temporary_revisions"')
await queryRunner.query(
'CREATE TABLE "revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))',
'CREATE TABLE "revisions_revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))',
)
await queryRunner.query(
'INSERT INTO "revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "temporary_revisions"',
'INSERT INTO "revisions_revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "temporary_revisions"',
)
await queryRunner.query('DROP TABLE "temporary_revisions"')
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ')
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ')
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions_revisions" ("item_uuid") ')
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions_revisions" ("user_uuid") ')
}
}

View File

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

View File

@@ -66,8 +66,13 @@ import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository'
import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper'
import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper'
import { RemoveRevisionsFromSharedVault } from '../Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault'
import { ItemRemovedFromSharedVaultEventHandler } from '../Domain/Handler/ItemRemovedFromSharedVaultEventHandler'
import { TransitionRequestedEventHandler } from '../Domain/Handler/TransitionRequestedEventHandler'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
async load(configuration?: {
controllerConatiner?: ControllerContainerInterface
directCallDomainEventPublisher?: DirectCallDomainEventPublisher
@@ -113,7 +118,7 @@ export class ContainerConfigLoader {
container.bind<TimerInterface>(TYPES.Revisions_Timer).toDynamicValue(() => new Timer())
const appDataSource = new AppDataSource(env)
const appDataSource = new AppDataSource({ env, runMigrations: this.mode === 'server' })
await appDataSource.initialize()
logger.debug('Database initialized')
@@ -346,6 +351,7 @@ export class ContainerConfigLoader {
: null,
container.get<TimerInterface>(TYPES.Revisions_Timer),
container.get<winston.Logger>(TYPES.Revisions_Logger),
env.get('MIGRATION_BATCH_SIZE', true) ? +env.get('MIGRATION_BATCH_SIZE', true) : 100,
),
)
container
@@ -358,6 +364,13 @@ export class ContainerConfigLoader {
container.get<DomainEventFactoryInterface>(TYPES.Revisions_DomainEventFactory),
),
)
container
.bind<RemoveRevisionsFromSharedVault>(TYPES.Revisions_RemoveRevisionsFromSharedVault)
.toConstantValue(
new RemoveRevisionsFromSharedVault(
container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
),
)
// env vars
container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
@@ -407,6 +420,7 @@ export class ContainerConfigLoader {
new ItemDumpedEventHandler(
container.get<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository),
container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
)
container
@@ -429,6 +443,7 @@ export class ContainerConfigLoader {
.bind<TransitionStatusUpdatedEventHandler>(TYPES.Revisions_TransitionStatusUpdatedEventHandler)
.toConstantValue(
new TransitionStatusUpdatedEventHandler(
container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository),
container.get<TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
),
@@ -437,12 +452,32 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
)
container
.bind<ItemRemovedFromSharedVaultEventHandler>(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)
.toConstantValue(
new ItemRemovedFromSharedVaultEventHandler(
container.get<RemoveRevisionsFromSharedVault>(TYPES.Revisions_RemoveRevisionsFromSharedVault),
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
)
container
.bind<TransitionRequestedEventHandler>(TYPES.Revisions_TransitionRequestedEventHandler)
.toConstantValue(
new TransitionRequestedEventHandler(
container.get<TriggerTransitionFromPrimaryToSecondaryDatabaseForUser>(
TYPES.Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
),
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Revisions_AccountDeletionRequestedEventHandler)],
['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Revisions_TransitionStatusUpdatedEventHandler)],
['ITEM_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)],
['TRANSITION_REQUESTED', container.get(TYPES.Revisions_TransitionRequestedEventHandler)],
])
if (isConfiguredForHomeServer) {

View File

@@ -12,7 +12,12 @@ export class AppDataSource {
private _dataSource: DataSource | undefined
private _secondaryDataSource: DataSource | undefined
constructor(private env: Env) {}
constructor(
private configuration: {
env: Env
runMigrations: boolean
},
) {}
getRepository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): Repository<Entity> {
if (!this._dataSource) {
@@ -39,20 +44,20 @@ export class AppDataSource {
}
get secondaryDataSource(): DataSource | undefined {
this.env.load()
this.configuration.env.load()
if (this.env.get('SECONDARY_DB_ENABLED', true) !== 'true') {
if (this.configuration.env.get('SECONDARY_DB_ENABLED', true) !== 'true') {
return undefined
}
this._secondaryDataSource = new DataSource({
type: 'mongodb',
host: this.env.get('MONGO_HOST'),
host: this.configuration.env.get('MONGO_HOST'),
authSource: 'admin',
port: parseInt(this.env.get('MONGO_PORT')),
username: this.env.get('MONGO_USERNAME'),
password: this.env.get('MONGO_PASSWORD', true),
database: this.env.get('MONGO_DATABASE'),
port: parseInt(this.configuration.env.get('MONGO_PORT')),
username: this.configuration.env.get('MONGO_USERNAME'),
password: this.configuration.env.get('MONGO_PASSWORD', true),
database: this.configuration.env.get('MONGO_DATABASE'),
entities: [MongoDBRevision],
retryWrites: false,
synchronize: true,
@@ -62,15 +67,16 @@ export class AppDataSource {
}
get dataSource(): DataSource {
this.env.load()
this.configuration.env.load()
const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql'
const isConfiguredForMySQL = this.configuration.env.get('DB_TYPE') === 'mysql'
const isConfiguredForHomeServerOrSelfHosting =
this.env.get('MODE', true) === 'home-server' || this.env.get('MODE', true) === 'self-hosted'
this.configuration.env.get('MODE', true) === 'home-server' ||
this.configuration.env.get('MODE', true) === 'self-hosted'
const maxQueryExecutionTime = this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
const maxQueryExecutionTime = this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const migrationsSourceDirectoryName = isConfiguredForMySQL
@@ -83,28 +89,28 @@ export class AppDataSource {
maxQueryExecutionTime,
entities: [isConfiguredForHomeServerOrSelfHosting ? SQLRevision : SQLLegacyRevision],
migrations: [`${__dirname}/../../migrations/${migrationsSourceDirectoryName}/*.js`],
migrationsRun: true,
logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
migrationsRun: this.configuration.runMigrations,
logging: <LoggerOptions>this.configuration.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
}
if (isConfiguredForMySQL) {
const inReplicaMode = this.env.get('DB_REPLICA_HOST', true) ? true : false
const inReplicaMode = this.configuration.env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: this.env.get('DB_HOST'),
port: parseInt(this.env.get('DB_PORT')),
username: this.env.get('DB_USERNAME'),
password: this.env.get('DB_PASSWORD'),
database: this.env.get('DB_DATABASE'),
host: this.configuration.env.get('DB_HOST'),
port: parseInt(this.configuration.env.get('DB_PORT')),
username: this.configuration.env.get('DB_USERNAME'),
password: this.configuration.env.get('DB_PASSWORD'),
database: this.configuration.env.get('DB_DATABASE'),
},
slaves: [
{
host: this.env.get('DB_REPLICA_HOST', true),
port: parseInt(this.env.get('DB_PORT')),
username: this.env.get('DB_USERNAME'),
password: this.env.get('DB_PASSWORD'),
database: this.env.get('DB_DATABASE'),
host: this.configuration.env.get('DB_REPLICA_HOST', true),
port: parseInt(this.configuration.env.get('DB_PORT')),
username: this.configuration.env.get('DB_USERNAME'),
password: this.configuration.env.get('DB_PASSWORD'),
database: this.configuration.env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
@@ -118,11 +124,11 @@ export class AppDataSource {
supportBigNumbers: true,
bigNumberStrings: false,
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : this.env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(this.env.get('DB_PORT')),
username: inReplicaMode ? undefined : this.env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : this.env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : this.env.get('DB_DATABASE'),
host: inReplicaMode ? undefined : this.configuration.env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(this.configuration.env.get('DB_PORT')),
username: inReplicaMode ? undefined : this.configuration.env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : this.configuration.env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : this.configuration.env.get('DB_DATABASE'),
}
this._dataSource = new DataSource(mySQLDataSourceOptions)
@@ -130,7 +136,7 @@ export class AppDataSource {
const sqliteDataSourceOptions: SqliteConnectionOptions = {
...commonDataSourceOptions,
type: 'sqlite',
database: this.env.get('DB_SQLITE_DATABASE_PATH'),
database: this.configuration.env.get('DB_SQLITE_DATABASE_PATH'),
enableWAL: true,
busyErrorRetry: 2000,
}

View File

@@ -4,4 +4,4 @@ import { Env } from './Env'
const env: Env = new Env()
env.load()
export const MigrationsDataSource = new AppDataSource(env).dataSource
export const MigrationsDataSource = new AppDataSource({ env, runMigrations: true }).dataSource

View File

@@ -49,6 +49,7 @@ const TYPES = {
Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
'Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser',
),
Revisions_RemoveRevisionsFromSharedVault: Symbol.for('Revisions_RemoveRevisionsFromSharedVault'),
// Controller
Revisions_ControllerContainer: Symbol.for('Revisions_ControllerContainer'),
Revisions_RevisionsController: Symbol.for('Revisions_RevisionsController'),
@@ -58,6 +59,8 @@ const TYPES = {
Revisions_AccountDeletionRequestedEventHandler: Symbol.for('Revisions_AccountDeletionRequestedEventHandler'),
Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
Revisions_TransitionStatusUpdatedEventHandler: Symbol.for('Revisions_TransitionStatusUpdatedEventHandler'),
Revisions_ItemRemovedFromSharedVaultEventHandler: Symbol.for('Revisions_ItemRemovedFromSharedVaultEventHandler'),
Revisions_TransitionRequestedEventHandler: Symbol.for('Revisions_TransitionRequestedEventHandler'),
// Services
Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
Revisions_DomainEventSubscriberFactory: Symbol.for('Revisions_DomainEventSubscriberFactory'),

View File

@@ -21,7 +21,10 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
},
origin: DomainEventService.SyncingServer,
},
payload: dto,
payload: {
transitionTimestamp: this.timer.getTimestampInMicroseconds(),
...dto,
},
}
}
}

View File

@@ -4,6 +4,6 @@ export interface DomainEventFactoryInterface {
createTransitionStatusUpdatedEvent(dto: {
userUuid: string
transitionType: 'items' | 'revisions'
status: 'STARTED' | 'FAILED' | 'FINISHED'
status: 'STARTED' | 'IN_PROGRESS' | 'FAILED' | 'FINISHED'
}): TransitionStatusUpdatedEvent
}

View File

@@ -1,4 +1,7 @@
import { ItemDumpedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { Uuid, ContentType, Dates } from '@standardnotes/domain-core'
import { DumpRepositoryInterface } from '../Dump/DumpRepositoryInterface'
import { Revision } from '../Revision/Revision'
import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
@@ -11,11 +14,22 @@ describe('ItemDumpedEventHandler', () => {
let revisionRepositoryResolver: RevisionRepositoryResolverInterface
let revision: Revision
let event: ItemDumpedEvent
let logger: Logger
const createHandler = () => new ItemDumpedEventHandler(dumpRepository, revisionRepositoryResolver)
const createHandler = () => new ItemDumpedEventHandler(dumpRepository, revisionRepositoryResolver, logger)
beforeEach(() => {
revision = {} as jest.Mocked<Revision>
revision = 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(),
}).getValue()
dumpRepository = {} as jest.Mocked<DumpRepositoryInterface>
dumpRepository.getRevisionFromDumpPath = jest.fn().mockReturnValue(revision)
@@ -32,6 +46,10 @@ describe('ItemDumpedEventHandler', () => {
fileDumpPath: 'foobar',
roleNames: ['CORE_USER'],
}
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.error = jest.fn()
})
it('should save a revision from file dump', async () => {

View File

@@ -3,16 +3,20 @@ import { DomainEventHandlerInterface, ItemDumpedEvent } from '@standardnotes/dom
import { DumpRepositoryInterface } from '../Dump/DumpRepositoryInterface'
import { RevisionRepositoryResolverInterface } from '../Revision/RevisionRepositoryResolverInterface'
import { RoleNameCollection } from '@standardnotes/domain-core'
import { Logger } from 'winston'
export class ItemDumpedEventHandler implements DomainEventHandlerInterface {
constructor(
private dumpRepository: DumpRepositoryInterface,
private revisionRepositoryResolver: RevisionRepositoryResolverInterface,
private logger: Logger,
) {}
async handle(event: ItemDumpedEvent): Promise<void> {
const revision = await this.dumpRepository.getRevisionFromDumpPath(event.payload.fileDumpPath)
if (revision === null) {
this.logger.error(`Revision not found for dump path ${event.payload.fileDumpPath}`)
await this.dumpRepository.removeDump(event.payload.fileDumpPath)
return
@@ -20,6 +24,8 @@ export class ItemDumpedEventHandler implements DomainEventHandlerInterface {
const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
if (roleNamesOrError.isFailed()) {
this.logger.error(`Invalid role names ${event.payload.roleNames}`)
await this.dumpRepository.removeDump(event.payload.fileDumpPath)
return
@@ -28,7 +34,10 @@ export class ItemDumpedEventHandler implements DomainEventHandlerInterface {
const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
await revisionRepository.insert(revision)
const successfullyInserted = await revisionRepository.insert(revision)
if (!successfullyInserted) {
this.logger.error(`Could not insert revision ${revision.id.toString()}`)
}
await this.dumpRepository.removeDump(event.payload.fileDumpPath)
}

View File

@@ -0,0 +1,22 @@
import { DomainEventHandlerInterface, ItemRemovedFromSharedVaultEvent } from '@standardnotes/domain-events'
import { RemoveRevisionsFromSharedVault } from '../UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault'
import { Logger } from 'winston'
export class ItemRemovedFromSharedVaultEventHandler implements DomainEventHandlerInterface {
constructor(
private removeRevisionsFromSharedVault: RemoveRevisionsFromSharedVault,
private logger: Logger,
) {}
async handle(event: ItemRemovedFromSharedVaultEvent): Promise<void> {
const result = await this.removeRevisionsFromSharedVault.execute({
sharedVaultUuid: event.payload.sharedVaultUuid,
itemUuid: event.payload.itemUuid,
roleNames: event.payload.roleNames,
})
if (result.isFailed()) {
this.logger.error(`Failed to remove revisions from shared vault: ${result.getError()}`)
}
}
}

View File

@@ -0,0 +1,27 @@
import { DomainEventHandlerInterface, TransitionRequestedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
export class TransitionRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
private triggerTransitionFromPrimaryToSecondaryDatabaseForUser: TriggerTransitionFromPrimaryToSecondaryDatabaseForUser,
private logger: Logger,
) {}
async handle(event: TransitionRequestedEvent): Promise<void> {
if (event.payload.type !== 'revisions') {
return
}
this.logger.info(`Handling transition requested event for user ${event.payload.userUuid}`)
const result = await this.triggerTransitionFromPrimaryToSecondaryDatabaseForUser.execute({
userUuid: event.payload.userUuid,
})
if (result.isFailed()) {
this.logger.error(`Failed to trigger transition for user ${event.payload.userUuid}`)
}
}
}

View File

@@ -6,9 +6,12 @@ import {
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from '../UseCase/Transition/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser'
import { Uuid } from '@standardnotes/domain-core'
import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerInterface {
constructor(
private primaryRevisionsRepository: RevisionRepositoryInterface,
private transitionRevisionsFromPrimaryToSecondaryDatabaseForUser: TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
@@ -29,6 +32,42 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
}
if (event.payload.status === 'STARTED' && event.payload.transitionType === 'revisions') {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.error(
`Failed to transition revisions for user ${event.payload.userUuid}: ${userUuidOrError.getError()}`,
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'FAILED',
transitionType: 'revisions',
}),
)
return
}
if (await this.isAlreadyMigrated(userUuidOrError.getValue())) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'FINISHED',
transitionType: 'revisions',
}),
)
return
}
await this.domainEventPublisher.publish(
this.domainEventFactory.createTransitionStatusUpdatedEvent({
userUuid: event.payload.userUuid,
status: 'IN_PROGRESS',
transitionType: 'revisions',
}),
)
const result = await this.transitionRevisionsFromPrimaryToSecondaryDatabaseForUser.execute({
userUuid: event.payload.userUuid,
})
@@ -58,4 +97,16 @@ export class TransitionStatusUpdatedEventHandler implements DomainEventHandlerIn
return
}
}
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
}
}

View File

@@ -12,7 +12,7 @@ export class Revision extends Entity<RevisionProps> {
}
isIdenticalTo(revision: Revision): boolean {
if (this._id.toString() !== revision._id.toString()) {
if (this._id.toString().toLowerCase() !== revision._id.toString().toLowerCase()) {
return false
}

View File

@@ -1,6 +1,8 @@
import { ContentType, Dates } from '@standardnotes/domain-core'
import { ContentType, Dates, Uuid } from '@standardnotes/domain-core'
export interface RevisionMetadataProps {
contentType: ContentType
itemUuid: Uuid
sharedVaultUuid: Uuid | null
dates: Dates
}

View File

@@ -13,4 +13,5 @@ export interface RevisionRepositoryInterface {
updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void>
findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Array<Revision>>
insert(revision: Revision): Promise<boolean>
clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void>
}

View File

@@ -0,0 +1,66 @@
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
import { RemoveRevisionsFromSharedVault } from './RemoveRevisionsFromSharedVault'
describe('RemoveRevisionsFromSharedVault', () => {
let revisionRepositoryResolver: RevisionRepositoryResolverInterface
let revisionRepository: RevisionRepositoryInterface
const createUseCase = () => new RemoveRevisionsFromSharedVault(revisionRepositoryResolver)
beforeEach(() => {
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
revisionRepository.clearSharedVaultAndKeySystemAssociations = jest.fn()
revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
})
it('should clear shared vault and key system associations', async () => {
const useCase = createUseCase()
await useCase.execute({
itemUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
roleNames: ['CORE_USER'],
})
expect(revisionRepository.clearSharedVaultAndKeySystemAssociations).toHaveBeenCalled()
})
it('should return error when shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
itemUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: 'invalid',
roleNames: ['CORE_USER'],
})
expect(result.isFailed()).toBe(true)
})
it('should return error when item uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
itemUuid: 'invalid',
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
roleNames: ['CORE_USER'],
})
expect(result.isFailed()).toBe(true)
})
it('should return error when role names are invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
itemUuid: '00000000-0000-0000-0000-000000000000',
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
roleNames: ['invalid'],
})
expect(result.isFailed()).toBe(true)
})
})

View File

@@ -0,0 +1,33 @@
import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
import { RemoveRevisionsFromSharedVaultDTO } from './RemoveRevisionsFromSharedVaultDTO'
export class RemoveRevisionsFromSharedVault implements UseCaseInterface<void> {
constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {}
async execute(dto: RemoveRevisionsFromSharedVaultDTO): Promise<Result<void>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const itemUuidOrError = Uuid.create(dto.itemUuid)
if (itemUuidOrError.isFailed()) {
return Result.fail(itemUuidOrError.getError())
}
const itemUuid = itemUuidOrError.getValue()
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
if (roleNamesOrError.isFailed()) {
return Result.fail(roleNamesOrError.getError())
}
const roleNames = roleNamesOrError.getValue()
const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
await revisionRepository.clearSharedVaultAndKeySystemAssociations(itemUuid, sharedVaultUuid)
return Result.ok()
}
}

View File

@@ -0,0 +1,5 @@
export interface RemoveRevisionsFromSharedVaultDTO {
itemUuid: string
sharedVaultUuid: string
roleNames: string[]
}

View File

@@ -1,437 +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,
)
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().mockResolvedValue(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(result.getError()).toEqual(
'Revision 00000000-0000-0000-0000-000000000001 is not identical in primary and secondary database',
)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
})
it('should remove Revisions from secondary database if migrating Revisions fails', async () => {
primaryRevisionRepository.findByUserUuid = jest
.fn()
.mockResolvedValueOnce([primaryRevision1])
.mockRejectedValueOnce(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Errored when migrating revisions for user 00000000-0000-0000-0000-000000000000: error',
)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
})
it('should return an error for a specific revision if it errors when saving to secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).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().mockResolvedValue(1)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Total revisions count for user 00000000-0000-0000-0000-000000000000 in primary database (2) does not match total revisions count in secondary database (1)',
)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(3)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid).toHaveBeenCalledTimes(1)
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if one Revision is not found in the secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).findOneByUuid = jest
.fn()
.mockResolvedValueOnce(secondaryRevision1)
.mockResolvedValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Revision 00000000-0000-0000-0000-000000000001 not found in secondary database')
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledTimes(3)
expect(primaryRevisionRepository.countByUserUuid).toHaveBeenCalledWith(
Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
)
expect((secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid).not.toHaveBeenCalled()
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if an error is thrown during integrity check between primary and secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).countByUserUuid = jest
.fn()
.mockRejectedValue(new Error('error'))
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Errored when checking integrity between primary and secondary database: error')
expect(primaryRevisionRepository.removeByUserUuid).not.toHaveBeenCalled()
expect((secondaryRevisionRepository as RevisionRepositoryInterface).removeByUserUuid).toHaveBeenCalledTimes(1)
})
it('should fail if a revisions did not save in the secondary database', async () => {
;(secondaryRevisionRepository as RevisionRepositoryInterface).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()
})
})

View File

@@ -1,9 +1,11 @@
/* istanbul ignore file */
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO } from './TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO'
import { RevisionRepositoryInterface } from '../../../Revision/RevisionRepositoryInterface'
import { Revision } from '../../../Revision/Revision'
export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements UseCaseInterface<void> {
constructor(
@@ -11,9 +13,12 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
private secondRevisionsRepository: RevisionRepositoryInterface | null,
private timer: TimerInterface,
private logger: Logger,
private pageSize: number,
) {}
async execute(dto: TransitionRevisionsFromPrimaryToSecondaryDatabaseForUserDTO): Promise<Result<void>> {
this.logger.info(`Transitioning revisions for user ${dto.userUuid}`)
if (this.secondRevisionsRepository === null) {
return Result.fail('Secondary revision repository is not set')
}
@@ -24,23 +29,41 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
}
const userUuid = userUuidOrError.getValue()
if (await this.isAlreadyMigrated(userUuid)) {
this.logger.info(`Revisions for user ${userUuid.value} are already migrated`)
let newRevisionsInSecondaryCount = 0
if (await this.hasAlreadyDataInSecondaryDatabase(userUuid)) {
const newRevisions = await this.getNewRevisionsCreatedInSecondaryDatabase(userUuid)
for (const existingRevision of newRevisions.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,
)
}
return Result.ok()
if (newRevisions.newRevisionsInSecondary.length > 0) {
this.logger.info(
`Found ${newRevisions.newRevisionsInSecondary.length} new revisions in secondary database for user ${userUuid.value}`,
)
}
newRevisionsInSecondaryCount = newRevisions.newRevisionsInSecondary.length
}
await this.allowForSecondaryDatabaseToCatchUp()
const migrationTimeStart = this.timer.getTimestampInMicroseconds()
this.logger.debug(`Transitioning revisions for user ${userUuid.value}`)
const migrationResult = await this.migrateRevisionsForUser(userUuid)
if (migrationResult.isFailed()) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
if (newRevisionsInSecondaryCount === 0) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
}
}
return Result.fail(migrationResult.getError())
@@ -48,13 +71,18 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
await this.allowForSecondaryDatabaseToCatchUp()
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid)
const integrityCheckResult = await this.checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid,
newRevisionsInSecondaryCount,
)
if (integrityCheckResult.isFailed()) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
if (newRevisionsInSecondaryCount === 0) {
const cleanupResult = await this.deleteRevisionsForUser(userUuid, this.secondRevisionsRepository)
if (cleanupResult.isFailed()) {
this.logger.error(
`Failed to clean up secondary database revisions for user ${userUuid.value}: ${cleanupResult.getError()}`,
)
}
}
return Result.fail(integrityCheckResult.getError())
@@ -83,13 +111,12 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
try {
const totalRevisionsCountForUser = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
let totalRevisionsCountTransitionedToSecondary = 0
const pageSize = 1
const totalPages = Math.ceil(totalRevisionsCountForUser / pageSize)
const totalPages = Math.ceil(totalRevisionsCountForUser / this.pageSize)
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
const query = {
userUuid: userUuid,
offset: (currentPage - 1) * pageSize,
limit: pageSize,
offset: (currentPage - 1) * this.pageSize,
limit: this.pageSize,
}
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
@@ -143,23 +170,84 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
await this.timer.sleep(twoSecondsInMilliseconds)
}
private async isAlreadyMigrated(userUuid: Uuid): Promise<boolean> {
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
private async hasAlreadyDataInSecondaryDatabase(userUuid: Uuid): Promise<boolean> {
const totalRevisionsCountForUserInSecondary = await (
this.secondRevisionsRepository as RevisionRepositoryInterface
).countByUserUuid(userUuid)
return totalRevisionsCountForUserInPrimary === 0
const hasAlreadyDataInSecondaryDatabase = totalRevisionsCountForUserInSecondary > 0
if (hasAlreadyDataInSecondaryDatabase) {
this.logger.info(
`User ${userUuid.value} has already ${totalRevisionsCountForUserInSecondary} revisions in secondary database`,
)
}
return hasAlreadyDataInSecondaryDatabase
}
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(userUuid: Uuid): Promise<Result<boolean>> {
private async getNewRevisionsCreatedInSecondaryDatabase(userUuid: Uuid): Promise<{
alreadyExistingInPrimary: Revision[]
newRevisionsInSecondary: Revision[]
}> {
const revisions = await (this.secondRevisionsRepository as RevisionRepositoryInterface).findByUserUuid({
userUuid: userUuid,
})
const alreadyExistingInPrimary: Revision[] = []
const newRevisionsInSecondary: Revision[] = []
for (const revision of revisions) {
const revisionExistsInPrimary = await this.checkIfRevisionExistsInPrimaryDatabase(revision)
if (revisionExistsInPrimary) {
alreadyExistingInPrimary.push(revision)
} else {
newRevisionsInSecondary.push(revision)
}
}
return {
alreadyExistingInPrimary: alreadyExistingInPrimary,
newRevisionsInSecondary: newRevisionsInSecondary,
}
}
private async checkIfRevisionExistsInPrimaryDatabase(revision: Revision): Promise<boolean> {
const revisionInPrimary = await this.primaryRevisionsRepository.findOneByUuid(
Uuid.create(revision.id.toString()).getValue(),
revision.props.userUuid as Uuid,
[],
)
if (revisionInPrimary === null) {
return false
}
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 false
}
return true
}
private async checkIntegrityBetweenPrimaryAndSecondaryDatabase(
userUuid: Uuid,
newRevisionsInSecondaryCount: number,
): Promise<Result<boolean>> {
try {
const totalRevisionsCountForUserInPrimary = await this.primaryRevisionsRepository.countByUserUuid(userUuid)
const pageSize = 1
const totalPages = Math.ceil(totalRevisionsCountForUserInPrimary / pageSize)
const totalPages = Math.ceil(totalRevisionsCountForUserInPrimary / this.pageSize)
for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
const query = {
userUuid: userUuid,
offset: (currentPage - 1) * pageSize,
limit: pageSize,
offset: (currentPage - 1) * this.pageSize,
limit: this.pageSize,
}
const revisions = await this.primaryRevisionsRepository.findByUserUuid(query)
@@ -180,7 +268,11 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
}
if (!revision.isIdenticalTo(revisionInSecondary)) {
return Result.fail(`Revision ${revision.id.toString()} is not identical in primary and secondary database`)
return Result.fail(
`Revision ${revision.id.toString()} is not identical in primary and secondary database. Revision in primary database: ${JSON.stringify(
revision,
)}, revision in secondary database: ${JSON.stringify(revisionInSecondary)}`,
)
}
}
}
@@ -189,9 +281,12 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
this.secondRevisionsRepository as RevisionRepositoryInterface
).countByUserUuid(userUuid)
if (totalRevisionsCountForUserInPrimary !== totalRevisionsCountForUserInSecondary) {
if (
totalRevisionsCountForUserInPrimary + newRevisionsInSecondaryCount !==
totalRevisionsCountForUserInSecondary
) {
return Result.fail(
`Total revisions count for user ${userUuid.value} in primary database (${totalRevisionsCountForUserInPrimary}) does not match total revisions count in secondary database (${totalRevisionsCountForUserInSecondary})`,
`Total revisions count for user ${userUuid.value} in primary database (${totalRevisionsCountForUserInPrimary} + ${newRevisionsInSecondaryCount}) does not match total revisions count in secondary database (${totalRevisionsCountForUserInSecondary})`,
)
}

View File

@@ -16,6 +16,21 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
private logger: Logger,
) {}
async clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void> {
await this.mongoRepository.updateMany(
{
itemUuid: { $eq: itemUuid.value },
sharedVaultUuid: { $eq: sharedVaultUuid.value },
},
{
$set: {
sharedVaultUuid: null,
keySystemIdentifier: null,
},
},
)
}
async countByUserUuid(userUuid: Uuid): Promise<number> {
return this.mongoRepository.count({ userUuid: { $eq: userUuid.value } })
}
@@ -111,7 +126,7 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
let persistence = []
if (sharedVaultUuids.length > 0) {
persistence = await this.mongoRepository.find({
select: ['_id', 'contentType', 'createdAt', 'updatedAt'],
select: ['_id', 'contentType', 'createdAt', 'updatedAt', 'sharedVaultUuid', 'itemUuid'],
where: {
$and: [
{ itemUuid: { $eq: itemUuid.value } },
@@ -129,7 +144,7 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
})
} else {
persistence = await this.mongoRepository.find({
select: ['_id', 'contentType', 'createdAt', 'updatedAt'],
select: ['_id', 'contentType', 'createdAt', 'updatedAt', 'sharedVaultUuid', 'itemUuid'],
where: {
$and: [{ itemUuid: { $eq: itemUuid.value } }, { userUuid: { $eq: userUuid.value } }],
},

View File

@@ -15,6 +15,10 @@ export class SQLLegacyRevisionRepository implements RevisionRepositoryInterface
protected logger: Logger,
) {}
async clearSharedVaultAndKeySystemAssociations(_itemUuid: Uuid, _sharedVaultUuid: Uuid): Promise<void> {
this.logger.error('Method clearSharedVaultAndKeySystemAssociations not implemented.')
}
async countByUserUuid(userUuid: Uuid): Promise<number> {
return this.ormRepository
.createQueryBuilder()
@@ -125,6 +129,7 @@ export class SQLLegacyRevisionRepository implements RevisionRepositoryInterface
.addSelect('content_type', 'contentType')
.addSelect('created_at', 'createdAt')
.addSelect('updated_at', 'updatedAt')
.addSelect('item_uuid', 'itemUuid')
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
.andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
.orderBy('created_at', 'DESC')

View File

@@ -2,7 +2,7 @@ import { Column, Entity, Index } from 'typeorm'
import { SQLLegacyRevision } from './SQLLegacyRevision'
@Entity({ name: 'revisions' })
@Entity({ name: 'revisions_revisions' })
export class SQLRevision extends SQLLegacyRevision {
@Column({
type: 'varchar',

View File

@@ -17,6 +17,27 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
super(ormRepository, revisionMetadataMapper, revisionMapper, logger)
}
override async removeByUserUuid(userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.from('revisions_revisions')
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
.execute()
}
override async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.from('revisions_revisions')
.where('uuid = :revisionUuid AND user_uuid = :userUuid', {
userUuid: userUuid.value,
revisionUuid: revisionUuid.value,
})
.execute()
}
override async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<Revision | null> {
const queryBuilder = this.ormRepository.createQueryBuilder()
@@ -45,6 +66,21 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
return this.revisionMapper.toDomain(sqlRevision)
}
override async clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.update()
.set({
sharedVaultUuid: null,
keySystemIdentifier: null,
})
.where('item_uuid = :itemUuid AND shared_vault_uuid = :sharedVaultUuid', {
itemUuid: itemUuid.value,
sharedVaultUuid: sharedVaultUuid.value,
})
.execute()
}
override async findMetadataByItemId(
itemUuid: Uuid,
userUuid: Uuid,
@@ -56,6 +92,8 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
.addSelect('content_type', 'contentType')
.addSelect('created_at', 'createdAt')
.addSelect('updated_at', 'updatedAt')
.addSelect('shared_vault_uuid', 'sharedVaultUuid')
.addSelect('item_uuid', 'itemUuid')
.orderBy('created_at', 'DESC')
if (sharedVaultUuids.length > 0) {

View File

@@ -15,10 +15,12 @@ export class RevisionMetadataHttpMapper
toProjection(domain: RevisionMetadata): RevisionMetadataHttpRepresentation {
return {
uuid: domain.id.toString(),
item_uuid: domain.props.itemUuid.value,
content_type: domain.props.contentType.value as string,
created_at: domain.props.dates.createdAt.toISOString(),
updated_at: domain.props.dates.updatedAt.toISOString(),
required_role: this.getRequiredRoleToViewRevision.execute({ createdAt: domain.props.dates.createdAt }).getValue(),
shared_vault_uuid: domain.props.sharedVaultUuid ? domain.props.sharedVaultUuid.value : null,
}
}
}

View File

@@ -1,7 +1,9 @@
export interface RevisionMetadataHttpRepresentation {
uuid: string
item_uuid: string
content_type: string
created_at: string
updated_at: string
required_role: string
shared_vault_uuid: string | null
}

View File

@@ -1,4 +1,4 @@
import { MapperInterface, Dates, UniqueEntityId, ContentType } from '@standardnotes/domain-core'
import { MapperInterface, Dates, UniqueEntityId, ContentType, Uuid } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
import { MongoDBRevision } from '../../../Infra/TypeORM/MongoDB/MongoDBRevision'
@@ -20,10 +20,27 @@ export class MongoDBRevisionMetadataPersistenceMapper implements MapperInterface
}
const dates = datesOrError.getValue()
let sharedVaultUuid = null
if (projection.sharedVaultUuid) {
const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
throw new Error(`Could not create shared vault uuid: ${sharedVaultUuidOrError.getError()}`)
}
sharedVaultUuid = sharedVaultUuidOrError.getValue()
}
const itemUuidOrError = Uuid.create(projection.itemUuid)
if (itemUuidOrError.isFailed()) {
throw new Error(`Could not create item uuid: ${itemUuidOrError.getError()}`)
}
const itemUuid = itemUuidOrError.getValue()
const revisionMetadataOrError = RevisionMetadata.create(
{
contentType,
dates,
sharedVaultUuid,
itemUuid,
},
new UniqueEntityId(projection._id.toHexString()),
)

View File

@@ -1,8 +1,10 @@
import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@standardnotes/domain-core'
import { BSON } from 'mongodb'
import { MongoDBRevision } from '../../../Infra/TypeORM/MongoDB/MongoDBRevision'
import { Revision } from '../../../Domain/Revision/Revision'
import { BSON } from 'mongodb'
import { SharedVaultAssociation } from '../../../Domain/SharedVault/SharedVaultAssociation'
import { KeySystemAssociation } from '../../../Domain/KeySystem/KeySystemAssociation'
export class MongoDBRevisionPersistenceMapper implements MapperInterface<Revision, MongoDBRevision> {
toDomain(projection: MongoDBRevision): Revision {
@@ -33,6 +35,39 @@ export class MongoDBRevisionPersistenceMapper implements MapperInterface<Revisio
userUuid = userUuidOrError.getValue()
}
let sharedVaultAssociation: SharedVaultAssociation | undefined = undefined
if (projection.sharedVaultUuid && projection.editedBy) {
const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
throw new Error(`Failed to create revision from projection: ${sharedVaultUuidOrError.getError()}`)
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const lastEditedByOrError = Uuid.create(projection.editedBy)
if (lastEditedByOrError.isFailed()) {
throw new Error(`Failed to create revision from projection: ${lastEditedByOrError.getError()}`)
}
const lastEditedBy = lastEditedByOrError.getValue()
const sharedVaultAssociationOrError = SharedVaultAssociation.create({
sharedVaultUuid,
editedBy: lastEditedBy,
})
if (sharedVaultAssociationOrError.isFailed()) {
throw new Error(`Failed to create revision from projection: ${sharedVaultAssociationOrError.getError()}`)
}
sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
}
let keySystemAssociation: KeySystemAssociation | undefined = undefined
if (projection.keySystemIdentifier) {
const keySystemAssociationOrError = KeySystemAssociation.create(projection.keySystemIdentifier)
if (keySystemAssociationOrError.isFailed()) {
throw new Error(`Failed to create revision from projection: ${keySystemAssociationOrError.getError()}`)
}
keySystemAssociation = keySystemAssociationOrError.getValue()
}
const revisionOrError = Revision.create(
{
authHash: projection.authHash,
@@ -44,6 +79,8 @@ export class MongoDBRevisionPersistenceMapper implements MapperInterface<Revisio
itemUuid,
userUuid,
dates,
sharedVaultAssociation,
keySystemAssociation,
},
new UniqueEntityId(projection._id.toHexString()),
)
@@ -67,6 +104,15 @@ export class MongoDBRevisionPersistenceMapper implements MapperInterface<Revisio
mongoDBRevision.itemUuid = domain.props.itemUuid.value
mongoDBRevision.itemsKeyId = domain.props.itemsKeyId
mongoDBRevision.userUuid = domain.props.userUuid ? domain.props.userUuid.value : null
mongoDBRevision.sharedVaultUuid = domain.props.sharedVaultAssociation
? domain.props.sharedVaultAssociation.props.sharedVaultUuid.value
: null
mongoDBRevision.editedBy = domain.props.sharedVaultAssociation
? domain.props.sharedVaultAssociation.props.editedBy.value
: null
mongoDBRevision.keySystemIdentifier = domain.props.keySystemAssociation
? domain.props.keySystemAssociation.props.keySystemIdentifier
: null
mongoDBRevision._id = BSON.UUID.createFromHexString(domain.id.toString())
return mongoDBRevision

View File

@@ -1,4 +1,4 @@
import { MapperInterface, Dates, UniqueEntityId, ContentType } from '@standardnotes/domain-core'
import { MapperInterface, Dates, UniqueEntityId, ContentType, Uuid } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
import { SQLLegacyRevision } from '../../../Infra/TypeORM/SQL/SQLLegacyRevision'
@@ -22,10 +22,18 @@ export class SQLLegacyRevisionMetadataPersistenceMapper
}
const dates = datesOrError.getValue()
const itemUuidOrError = Uuid.create(projection.itemUuid)
if (itemUuidOrError.isFailed()) {
throw new Error(`Could not create item uuid: ${itemUuidOrError.getError()}`)
}
const itemUuid = itemUuidOrError.getValue()
const revisionMetadataOrError = RevisionMetadata.create(
{
contentType,
dates,
sharedVaultUuid: null,
itemUuid,
},
new UniqueEntityId(projection.uuid),
)

View File

@@ -1,4 +1,4 @@
import { MapperInterface, Dates, UniqueEntityId, ContentType } from '@standardnotes/domain-core'
import { MapperInterface, Dates, UniqueEntityId, ContentType, Uuid } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
import { SQLRevision } from '../../../Infra/TypeORM/SQL/SQLRevision'
@@ -20,10 +20,27 @@ export class SQLRevisionMetadataPersistenceMapper implements MapperInterface<Rev
}
const dates = datesOrError.getValue()
let sharedVaultUuid = null
if (projection.sharedVaultUuid) {
const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
throw new Error(`Could not create shared vault uuid: ${sharedVaultUuidOrError.getError()}`)
}
sharedVaultUuid = sharedVaultUuidOrError.getValue()
}
const itemUuidOrError = Uuid.create(projection.itemUuid)
if (itemUuidOrError.isFailed()) {
throw new Error(`Could not create item uuid: ${itemUuidOrError.getError()}`)
}
const itemUuid = itemUuidOrError.getValue()
const revisionMetadataOrError = RevisionMetadata.create(
{
contentType,
dates,
sharedVaultUuid,
itemUuid,
},
new UniqueEntityId(projection.uuid),
)

View File

@@ -2,6 +2,5 @@
set -euo pipefail
sh supervisor/wait-for.sh $DB_HOST $DB_PORT
sh supervisor/wait-for.sh $REDIS_HOST $REDIS_PORT
sh supervisor/wait-for.sh localhost $SYNCING_SERVER_PORT
node docker/entrypoint-server.js

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.42](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.41...@standardnotes/scheduler-server@1.20.42) (2023-09-12)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.41](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.40...@standardnotes/scheduler-server@1.20.41) (2023-09-12)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.40](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.39...@standardnotes/scheduler-server@1.20.40) (2023-09-12)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.39](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.38...@standardnotes/scheduler-server@1.20.39) (2023-09-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.38](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.37...@standardnotes/scheduler-server@1.20.38) (2023-09-07)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.37](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.36...@standardnotes/scheduler-server@1.20.37) (2023-09-07)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

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

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.1](https://github.com/standardnotes/server/compare/@standardnotes/security@1.13.0...@standardnotes/security@1.13.1) (2023-09-12)
### Bug Fixes
* adjust transitions to not create revisions during ongoing revisions transition ([106d8f9](https://github.com/standardnotes/server/commit/106d8f9192f630794ca4ddc2c4503f2c6cd196e7))
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.12.2...@standardnotes/security@1.13.0) (2023-09-06)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.13.0",
"version": "1.13.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

Some files were not shown because too many files have changed in this diff Show More