Compare commits

..

25 Commits

Author SHA1 Message Date
standardci e98393452b chore(release): publish new version
- @standardnotes/analytics@2.25.14
 - @standardnotes/api-gateway@1.70.5
 - @standardnotes/auth-server@1.132.0
 - @standardnotes/domain-core@1.26.0
 - @standardnotes/event-store@1.11.20
 - @standardnotes/files-server@1.20.4
 - @standardnotes/home-server@1.14.0
 - @standardnotes/revisions-server@1.26.8
 - @standardnotes/scheduler-server@1.20.23
 - @standardnotes/settings@1.21.25
 - @standardnotes/syncing-server@1.80.0
 - @standardnotes/websockets-server@1.10.18
2023-08-18 15:15:20 +00:00
Karol Sójko 302b624504 feat: add mechanism for determining if a user should use the primary or secondary items database (#700)
* feat(domain-core): introduce new role for users transitioning to new mechanisms

* feat: add mechanism for determining if a user should use the primary or secondary items database

* fix: add transition mode enabled switch in docker entrypoint

* fix(syncing-server): mapping roles from middleware

* fix: mongodb item repository binding

* fix: item backups service binding

* fix: passing transition mode enabled variable to docker setup
2023-08-18 16:45:10 +02:00
Karol Sójko e00d9d2ca0 fix: e2e parameter for running vault tests 2023-08-18 11:11:54 +02:00
Karol Sójko 9ab4601c8d feat: add transition mode switch to e2e test suite 2023-08-18 11:00:36 +02:00
Karol Sójko 19e43bdb1a fix: run vault tests based on secondary db usage (#699) 2023-08-17 13:21:50 +02:00
standardci 49832e7944 chore(release): publish new version
- @standardnotes/home-server@1.13.51
 - @standardnotes/syncing-server@1.79.1
2023-08-17 10:15:43 +00:00
Karol Sójko 916e98936a fix(home-server): add default env values for secondary database 2023-08-17 11:56:56 +02:00
Karol Sójko 31d1eef7f7 fix(syncing-server): refactor shared vault and key system associations (#698)
* feat(syncing-server): refactor persistence of shared vault and key system associations

* fix(syncing-server): refactor shared vault and key system associations
2023-08-17 11:56:16 +02:00
standardci 2648d9a813 chore(release): publish new version
- @standardnotes/home-server@1.13.50
 - @standardnotes/syncing-server@1.79.0
2023-08-16 11:16:38 +00:00
Karol Sójko b24b576209 feat: add mongodb initial support (#696)
* feat: add mongodb initial support

* fix: typeorm annotations for mongodb entity

* wip mongo repo

* feat: add mongodb queries

* fix(syncing-server): env sample

* fix(syncing-server): Mongo connection auth source

* fix(syncing-server): db switch env var name

* fix(syncing-server): persisting and querying by _id as UUID in MongoDB

* fix(syncing-server): items upserts on MongoDB

* fix: remove foreign key migration
2023-08-16 13:00:16 +02:00
Karol Sójko faee38bffd fix: hosts for home-server e2e ci setup 2023-08-15 13:17:20 +02:00
Karol Sójko 65f3503fe8 fix: docker compose ci setup 2023-08-15 13:11:14 +02:00
Karol Sójko 054023b791 fix: host variables 2023-08-15 12:59:13 +02:00
Karol Sójko 383c3a68fa fix: default value for SECONDARY_DB_ENABLED 2023-08-15 12:56:55 +02:00
Karol Sójko 7d22b1c15c feat: run mongo db secondary database in e2e 2023-08-15 12:50:38 +02:00
standardci c71e7cd926 chore(release): publish new version
- @standardnotes/auth-server@1.131.5
 - @standardnotes/home-server@1.13.49
2023-08-15 10:34:11 +00:00
Karol Sójko 83ad069c5d fix(auth): passing the invalidate cache header (#697) 2023-08-15 12:16:01 +02:00
standardci 081108d9ba chore(release): publish new version
- @standardnotes/home-server@1.13.48
 - @standardnotes/syncing-server@1.78.11
2023-08-11 11:52:27 +00:00
Karol Sójko 8f3df56a2b chore: fix revisions frequency 2023-08-11 13:22:11 +02:00
Karol Sójko d02124f4e5 Revert "tmp: disable shared vaults"
This reverts commit c49dc35ab5.
2023-08-11 12:28:57 +02:00
Karol Sójko 09e351fedb Revert "tmp: ci"
This reverts commit 06cedd11d8.
2023-08-11 12:27:12 +02:00
Karol Sójko ad4b85b095 Revert "tmp: disable decorating with associations on revisions"
This reverts commit ac3646836c.
2023-08-11 12:26:44 +02:00
Karol Sójko 0bf7d8beae Revert "tmp: disable decorating items completely"
This reverts commit bc1c7a8ae1.
2023-08-11 12:25:35 +02:00
standardci 1ae7cca394 chore(release): publish new version
- @standardnotes/home-server@1.13.47
 - @standardnotes/syncing-server@1.78.10
2023-08-11 09:00:00 +00:00
Karol Sójko bc1c7a8ae1 tmp: disable decorating items completely 2023-08-11 10:54:12 +02:00
121 changed files with 2201 additions and 879 deletions
+6
View File
@@ -22,6 +22,12 @@ MYSQL_USER=std_notes_user
MYSQL_PASSWORD=changeme123
MYSQL_ROOT_PASSWORD=changeme123
MONGO_HOST=secondary_db
MONGO_PORT=27017
MONGO_USERNAME=standardnotes
MONGO_PASSWORD=standardnotes
MONGO_DATABASE=standardnotes
AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
+52 -17
View File
@@ -20,6 +20,11 @@ on:
jobs:
e2e:
name: (Docker) E2E Test Suite
strategy:
fail-fast: false
matrix:
secondary_db_enabled: [true, false]
transition_mode_enabled: [true, false]
runs-on: ubuntu-latest
services:
@@ -45,12 +50,23 @@ jobs:
env:
DB_TYPE: mysql
CACHE_TYPE: redis
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
TRANSITION_MODE_ENABLED: ${{ matrix.transition_mode_enabled }}
- name: Wait for server to start
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
- name: Define if vault tests are enabled
id: vaults
run: |
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
else
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
fi
- name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
- name: Show logs on failure
if: ${{ failure() }}
@@ -67,13 +83,8 @@ jobs:
matrix:
db_type: [mysql, sqlite]
cache_type: [redis, memory]
include:
- cache_type: redis
db_type: mysql
redis_port: 6380
- cache_type: redis
db_type: sqlite
redis_port: 6381
secondary_db_enabled: [true, false]
transition_mode_enabled: [true, false]
runs-on: ubuntu-latest
@@ -85,16 +96,24 @@ jobs:
cache:
image: redis
ports:
- ${{ matrix.redis_port }}:6379
- 6379:6379
db:
image: mysql
ports:
- 3307:3306
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: standardnotes_${{ matrix.cache_type }}
MYSQL_DATABASE: standardnotes
MYSQL_USER: standardnotes
MYSQL_PASSWORD: standardnotes
secondary_db:
image: mongo:5.0
ports:
- 27017:27017
env:
MONGO_INITDB_ROOT_USERNAME: standardnotes
MONGO_INITDB_ROOT_PASSWORD: standardnotes
MONGO_INITDB_DATABASE: standardnotes
steps:
- uses: actions/checkout@v3
@@ -123,16 +142,23 @@ jobs:
sed -i "s/VALET_TOKEN_SECRET=/VALET_TOKEN_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
echo "DB_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3307" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes_${{ matrix.cache_type }}" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=sqlite_${{ matrix.cache_type }}.db" >> packages/home-server/.env
echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=homeserver.db" >> packages/home-server/.env
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 "REDIS_URL=redis://localhost:${{ matrix.redis_port }}" >> 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
echo "TRANSITION_MODE_ENABLED=${{ matrix.transition_mode_enabled }}" >> packages/home-server/.env
echo "MONGO_HOST=localhost" >> packages/home-server/.env
echo "MONGO_PORT=27017" >> packages/home-server/.env
echo "MONGO_DATABASE=standardnotes" >> packages/home-server/.env
echo "MONGO_USERNAME=standardnotes" >> packages/home-server/.env
echo "MONGO_PASSWORD=standardnotes" >> packages/home-server/.env
echo "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
echo "E2E_TESTING=true" >> packages/home-server/.env
@@ -144,8 +170,17 @@ jobs:
- name: Wait for server to start
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
- name: Define if vault tests are enabled
id: vaults
run: |
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
else
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
fi
- name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
- name: Show logs on failure
if: ${{ failure() }}
+7 -7
View File
@@ -95,20 +95,20 @@ jobs:
- name: Test
run: yarn test
# e2e:
# needs: build
# name: E2E
# uses: standardnotes/server/.github/workflows/common-e2e.yml@main
# secrets: inherit
e2e:
needs: build
name: E2E
uses: standardnotes/server/.github/workflows/common-e2e.yml@main
secrets: inherit
publish-self-hosting:
needs: [ test, lint ]
needs: [ test, lint, e2e ]
name: Publish Self Hosting Docker Image
uses: standardnotes/server/.github/workflows/common-self-hosting.yml@main
secrets: inherit
publish-services:
needs: [ test, lint ]
needs: [ test, lint, e2e ]
runs-on: ubuntu-latest
Generated
+229 -1
View File
@@ -5191,6 +5191,7 @@ const RAW_RUNTIME_STATE =
["inversify-express-utils", "npm:6.4.3"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
["jsonwebtoken", "npm:9.0.0"],\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["mysql2", "npm:3.3.3"],\
["newrelic", "npm:10.1.2"],\
["nodemon", "npm:2.0.22"],\
@@ -5201,7 +5202,7 @@ const RAW_RUNTIME_STATE =
["semver", "npm:7.5.1"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.16"],\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["ua-parser-js", "npm:1.0.35"],\
["uuid", "npm:9.0.0"],\
@@ -5869,6 +5870,26 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/webidl-conversions", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@types-webidl-conversions-npm-7.0.0-0903313151-86c337dc1e.zip/node_modules/@types/webidl-conversions/",\
"packageDependencies": [\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/whatwg-url", [\
["npm:8.2.2", {\
"packageLocation": "./.yarn/cache/@types-whatwg-url-npm-8.2.2-54c5c24e6c-25f20f5649.zip/node_modules/@types/whatwg-url/",\
"packageDependencies": [\
["@types/whatwg-url", "npm:8.2.2"],\
["@types/node", "npm:20.2.5"],\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/yargs", [\
["npm:17.0.24", {\
"packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-f7811cc0b9.zip/node_modules/@types/yargs/",\
@@ -7074,6 +7095,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bson", [\
["npm:5.4.0", {\
"packageLocation": "./.yarn/cache/bson-npm-5.4.0-2f854c8216-2c913a45c0.zip/node_modules/bson/",\
"packageDependencies": [\
["bson", "npm:5.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["buffer", [\
["npm:5.7.1", {\
"packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-8e611bed4d.zip/node_modules/buffer/",\
@@ -11932,6 +11962,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["memory-pager", [\
["npm:1.5.0", {\
"packageLocation": "./.yarn/cache/memory-pager-npm-1.5.0-46e20e6c81-6b00ff499b.zip/node_modules/memory-pager/",\
"packageDependencies": [\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["meow", [\
["npm:8.1.2", {\
"packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-e36c879078.zip/node_modules/meow/",\
@@ -12290,6 +12329,59 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["mongodb", [\
["npm:5.7.0", {\
"packageLocation": "./.yarn/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "npm:5.7.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0", {\
"packageLocation": "./.yarn/__virtual__/mongodb-virtual-eb0cd47e23/0/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["@aws-sdk/credential-providers", null],\
["@mongodb-js/zstd", null],\
["@types/aws-sdk__credential-providers", null],\
["@types/kerberos", null],\
["@types/mongodb-client-encryption", null],\
["@types/mongodb-js__zstd", null],\
["@types/snappy", null],\
["bson", "npm:5.4.0"],\
["kerberos", null],\
["mongodb-client-encryption", null],\
["mongodb-connection-string-url", "npm:2.6.0"],\
["saslprep", "npm:1.0.3"],\
["snappy", null],\
["socks", "npm:2.7.1"]\
],\
"packagePeers": [\
"@aws-sdk/credential-providers",\
"@mongodb-js/zstd",\
"@types/aws-sdk__credential-providers",\
"@types/kerberos",\
"@types/mongodb-client-encryption",\
"@types/mongodb-js__zstd",\
"@types/snappy",\
"kerberos",\
"mongodb-client-encryption",\
"snappy"\
],\
"linkType": "HARD"\
}]\
]],\
["mongodb-connection-string-url", [\
["npm:2.6.0", {\
"packageLocation": "./.yarn/cache/mongodb-connection-string-url-npm-2.6.0-af011ba17f-8a9186dd1b.zip/node_modules/mongodb-connection-string-url/",\
"packageDependencies": [\
["mongodb-connection-string-url", "npm:2.6.0"],\
["@types/whatwg-url", "npm:8.2.2"],\
["whatwg-url", "npm:11.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["ms", [\
["npm:2.0.0", {\
"packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-de027828fc.zip/node_modules/ms/",\
@@ -14249,6 +14341,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["saslprep", [\
["npm:1.0.3", {\
"packageLocation": "./.yarn/cache/saslprep-npm-1.0.3-8db649c346-23ebcda091.zip/node_modules/saslprep/",\
"packageDependencies": [\
["saslprep", "npm:1.0.3"],\
["sparse-bitfield", "npm:3.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["schema-utils", [\
["npm:3.1.2", {\
"packageLocation": "./.yarn/cache/schema-utils-npm-3.1.2-d97c6dc247-11d35f997e.zip/node_modules/schema-utils/",\
@@ -14604,6 +14706,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["sparse-bitfield", [\
["npm:3.0.3", {\
"packageLocation": "./.yarn/cache/sparse-bitfield-npm-3.0.3-cb80d0c89f-625ecdf6f4.zip/node_modules/sparse-bitfield/",\
"packageDependencies": [\
["sparse-bitfield", "npm:3.0.3"],\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["spawn-please", [\
["npm:2.0.1", {\
"packageLocation": "./.yarn/cache/spawn-please-npm-2.0.1-265b6b5432-fe19a7ceb5.zip/node_modules/spawn-please/",\
@@ -15246,6 +15358,14 @@ const RAW_RUNTIME_STATE =
["tr46", "npm:0.0.3"]\
],\
"linkType": "HARD"\
}],\
["npm:3.0.0", {\
"packageLocation": "./.yarn/cache/tr46-npm-3.0.0-e1ae1ea7c9-3a481676bf.zip/node_modules/tr46/",\
"packageDependencies": [\
["tr46", "npm:3.0.0"],\
["punycode", "npm:2.3.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["treeverse", [\
@@ -15757,6 +15877,98 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-13b6364fde/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\
["@types/better-sqlite3", null],\
["@types/google-cloud__spanner", null],\
["@types/hdb-pool", null],\
["@types/ioredis", null],\
["@types/mongodb", null],\
["@types/mssql", null],\
["@types/mysql2", null],\
["@types/oracledb", null],\
["@types/pg", null],\
["@types/pg-native", null],\
["@types/pg-query-stream", null],\
["@types/redis", null],\
["@types/sap__hana-client", null],\
["@types/sql.js", null],\
["@types/sqlite3", null],\
["@types/ts-node", null],\
["@types/typeorm-aurora-data-api-driver", null],\
["app-root-path", "npm:3.1.0"],\
["better-sqlite3", null],\
["buffer", "npm:6.0.3"],\
["chalk", "npm:4.1.2"],\
["cli-highlight", "npm:2.1.11"],\
["date-fns", "npm:2.30.0"],\
["debug", "virtual:ac3d8e680759ce54399273724d44e041d6c9b73454d191d411a8c44bb27e22f02aaf6ed9d3ad0ac1c298eac4833cff369c9c7b84c573016112c4f84be2cd8543#npm:4.3.4"],\
["dotenv", "npm:16.1.3"],\
["glob", "npm:8.1.0"],\
["hdb-pool", null],\
["ioredis", null],\
["mkdirp", "npm:2.1.6"],\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["mssql", null],\
["mysql2", "npm:3.3.3"],\
["oracledb", null],\
["pg", null],\
["pg-native", null],\
["pg-query-stream", null],\
["redis", null],\
["reflect-metadata", "npm:0.1.13"],\
["sha.js", "npm:2.4.11"],\
["sql.js", null],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-node", null],\
["tslib", "npm:2.5.2"],\
["typeorm-aurora-data-api-driver", null],\
["uuid", "npm:9.0.0"],\
["yargs", "npm:17.7.2"]\
],\
"packagePeers": [\
"@google-cloud/spanner",\
"@sap/hana-client",\
"@types/better-sqlite3",\
"@types/google-cloud__spanner",\
"@types/hdb-pool",\
"@types/ioredis",\
"@types/mongodb",\
"@types/mssql",\
"@types/mysql2",\
"@types/oracledb",\
"@types/pg-native",\
"@types/pg-query-stream",\
"@types/pg",\
"@types/redis",\
"@types/sap__hana-client",\
"@types/sql.js",\
"@types/sqlite3",\
"@types/ts-node",\
"@types/typeorm-aurora-data-api-driver",\
"better-sqlite3",\
"hdb-pool",\
"ioredis",\
"mongodb",\
"mssql",\
"mysql2",\
"oracledb",\
"pg-native",\
"pg-query-stream",\
"pg",\
"redis",\
"sql.js",\
"sqlite3",\
"ts-node",\
"typeorm-aurora-data-api-driver"\
],\
"linkType": "HARD"\
}],\
["virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-fc9b7b780b/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\
@@ -16191,6 +16403,13 @@ const RAW_RUNTIME_STATE =
["webidl-conversions", "npm:3.0.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/webidl-conversions-npm-7.0.0-e8c8e30c68-bdbe11c68c.zip/node_modules/webidl-conversions/",\
"packageDependencies": [\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["webpack", [\
@@ -16249,6 +16468,15 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["whatwg-url", [\
["npm:11.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-11.0.0-073529d93a-ee3a532bfb.zip/node_modules/whatwg-url/",\
"packageDependencies": [\
["whatwg-url", "npm:11.0.0"],\
["tr46", "npm:3.0.0"],\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-bd0cc6b75b.zip/node_modules/whatwg-url/",\
"packageDependencies": [\
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
View File
@@ -23,6 +23,8 @@ services:
environment:
DB_TYPE: "${DB_TYPE}"
CACHE_TYPE: "${CACHE_TYPE}"
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
TRANSITION_MODE_ENABLED: "${TRANSITION_MODE_ENABLED}"
container_name: server-ci
ports:
- 3123:3000
@@ -61,6 +63,21 @@ services:
networks:
- standardnotes_self_hosted
secondary_db:
image: mongo:5.0
container_name: secondary_db-ci
expose:
- 27017
restart: unless-stopped
volumes:
- ./data/mongo:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: standardnotes
MONGO_INITDB_ROOT_PASSWORD: standardnotes
MONGO_INITDB_DATABASE: standardnotes
networks:
- standardnotes_self_hosted
cache:
image: redis:6.0-alpine
container_name: cache-ci
+6
View File
@@ -63,6 +63,12 @@ fi
if [ -z "$CACHE_TYPE" ]; then
export CACHE_TYPE="redis"
fi
if [ -z "$SECONDARY_DB_ENABLED" ]; then
export SECONDARY_DB_ENABLED=false
fi
if [ -z "$TRANSITION_MODE_ENABLED" ]; then
export TRANSITION_MODE_ENABLED=false
fi
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
#########
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.25.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.13...@standardnotes/analytics@2.25.14) (2023-08-18)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.12...@standardnotes/analytics@2.25.13) (2023-08-11)
**Note:** Version bump only for package @standardnotes/analytics
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.25.13",
"version": "2.25.14",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.70.4",
"version": "1.70.5",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.5...@standardnotes/auth-server@1.132.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.131.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.4...@standardnotes/auth-server@1.131.5) (2023-08-15)
### Bug Fixes
* **auth:** passing the invalidate cache header ([#697](https://github.com/standardnotes/server/issues/697)) ([83ad069](https://github.com/standardnotes/server/commit/83ad069c5dd9afa3a6db881f0d8a55a58d0642aa))
## [1.131.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.3...@standardnotes/auth-server@1.131.4) (2023-08-11)
**Note:** Version bump only for package @standardnotes/auth-server
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddTransitionRole1692348191367 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
)
}
public async down(): Promise<void> {
return
}
}
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddTransitionRole1692348280258 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
)
}
public async down(): Promise<void> {
return
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.131.4",
"version": "1.132.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+3
View File
@@ -560,6 +560,9 @@ export class ContainerConfigLoader {
container
.bind(TYPES.Auth_READONLY_USERS)
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
container
.bind(TYPES.Auth_TRANSITION_MODE_ENABLED)
.toConstantValue(env.get('TRANSITION_MODE_ENABLED', true) === 'true')
if (isConfiguredForInMemoryCache) {
container
+1
View File
@@ -101,6 +101,7 @@ const TYPES = {
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
// use cases
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
@@ -11,6 +11,7 @@ import { Register } from './Register'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { Session } from '../Session/Session'
import { RoleName } from '@standardnotes/domain-core'
describe('Register', () => {
let userRepository: UserRepositoryInterface
@@ -20,9 +21,19 @@ describe('Register', () => {
let user: User
let crypter: CrypterInterface
let timer: TimerInterface
let transitionModeEnabled = false
const createUseCase = () =>
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer)
new Register(
userRepository,
roleRepository,
authResponseFactory,
crypter,
false,
settingService,
timer,
transitionModeEnabled,
)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
@@ -75,6 +86,7 @@ describe('Register', () => {
updatedWithUserAgent: 'Mozilla',
uuid: expect.any(String),
version: '004',
roles: Promise.resolve([]),
createdAt: new Date(1),
updatedAt: new Date(1),
})
@@ -118,6 +130,48 @@ describe('Register', () => {
})
})
it('should register a new user with default role and transition role', async () => {
transitionModeEnabled = true
const role = new Role()
role.name = RoleName.NAMES.CoreUser
const transitionRole = new Role()
transitionRole.name = RoleName.NAMES.TransitionUser
roleRepository.findOneByName = jest.fn().mockReturnValueOnce(role).mockReturnValueOnce(transitionRole)
expect(
await createUseCase().execute({
email: 'test@test.te',
password: 'asdzxc',
updatedWithUserAgent: 'Mozilla',
apiVersion: '20200115',
ephemeralSession: false,
version: '004',
pwCost: 11,
pwSalt: 'qweqwe',
pwNonce: undefined,
}),
).toEqual({ success: true, authResponse: { foo: 'bar' } })
expect(userRepository.save).toHaveBeenCalledWith({
email: 'test@test.te',
encryptedPassword: expect.any(String),
encryptedServerKey: 'test',
serverEncryptionVersion: 1,
pwCost: 11,
pwNonce: undefined,
pwSalt: 'qweqwe',
updatedWithUserAgent: 'Mozilla',
uuid: expect.any(String),
version: '004',
createdAt: new Date(1),
updatedAt: new Date(1),
roles: Promise.resolve([role, transitionRole]),
})
})
it('should fail to register if username is invalid', async () => {
expect(
await createUseCase().execute({
@@ -195,6 +249,7 @@ describe('Register', () => {
true,
settingService,
timer,
transitionModeEnabled,
).execute({
email: 'test@test.te',
password: 'asdzxc',
+12 -3
View File
@@ -1,8 +1,9 @@
import * as bcrypt from 'bcryptjs'
import { RoleName, Username } from '@standardnotes/domain-core'
import { v4 as uuidv4 } from 'uuid'
import { inject, injectable } from 'inversify'
import { TimerInterface } from '@standardnotes/time'
import TYPES from '../../Bootstrap/Types'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
@@ -11,7 +12,6 @@ import { RegisterResponse } from './RegisterResponse'
import { UseCaseInterface } from './UseCaseInterface'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { TimerInterface } from '@standardnotes/time'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
@@ -27,6 +27,7 @@ export class Register implements UseCaseInterface {
@inject(TYPES.Auth_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_TRANSITION_MODE_ENABLED) private transitionModeEnabled: boolean,
) {}
async execute(dto: RegisterDTO): Promise<RegisterResponse> {
@@ -72,10 +73,18 @@ export class Register implements UseCaseInterface {
user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey()
user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION
const roles = []
const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser)
if (defaultRole) {
user.roles = Promise.resolve([defaultRole])
roles.push(defaultRole)
}
if (this.transitionModeEnabled) {
const transitionRole = await this.roleRepository.findOneByName(RoleName.NAMES.TransitionUser)
if (transitionRole) {
roles.push(transitionRole)
}
}
user.roles = Promise.resolve(roles)
Object.assign(user, registrationFields)
@@ -285,6 +285,10 @@ export class BaseAuthController extends BaseHttpController {
authorizationHeader: <string>request.headers.authorization,
})
if (result.headers?.has('x-invalidate-cache')) {
response.setHeader('x-invalidate-cache', result.headers.get('x-invalidate-cache') as string)
}
return this.json(result.data, result.status)
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.2...@standardnotes/domain-core@1.26.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.1...@standardnotes/domain-core@1.25.2) (2023-08-09)
### Reverts
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.25.2",
"version": "1.26.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -21,25 +21,36 @@ describe('RoleName', () => {
const plusUserRole = RoleName.create(RoleName.NAMES.PlusUser).getValue()
const coreUser = RoleName.create(RoleName.NAMES.CoreUser).getValue()
const internalTeamUser = RoleName.create(RoleName.NAMES.InternalTeamUser).getValue()
const transitionUser = RoleName.create(RoleName.NAMES.TransitionUser).getValue()
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(proUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(plusUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(plusUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
expect(plusUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
expect(plusUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(plusUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(coreUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(coreUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(transitionUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(transitionUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
expect(transitionUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
expect(transitionUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(transitionUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
})
})
@@ -8,6 +8,7 @@ export class RoleName extends ValueObject<RoleNameProps> {
PlusUser: 'PLUS_USER',
ProUser: 'PRO_USER',
InternalTeamUser: 'INTERNAL_TEAM_USER',
TransitionUser: 'TRANSITION_USER',
}
get value(): string {
@@ -19,11 +20,19 @@ export class RoleName extends ValueObject<RoleNameProps> {
case RoleName.NAMES.InternalTeamUser:
return true
case RoleName.NAMES.ProUser:
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser].includes(roleName.value)
return [
RoleName.NAMES.CoreUser,
RoleName.NAMES.PlusUser,
RoleName.NAMES.ProUser,
RoleName.NAMES.TransitionUser,
].includes(roleName.value)
case RoleName.NAMES.PlusUser:
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser].includes(roleName.value)
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.TransitionUser].includes(
roleName.value,
)
case RoleName.NAMES.CoreUser:
return [RoleName.NAMES.CoreUser].includes(roleName.value)
case RoleName.NAMES.TransitionUser:
return [RoleName.NAMES.CoreUser, RoleName.NAMES.TransitionUser].includes(roleName.value)
/*istanbul ignore next*/
default:
throw new Error(`Invalid role name: ${this.value}`)
@@ -3,32 +3,24 @@ import { RoleNameCollection } from './RoleNameCollection'
describe('RoleNameCollection', () => {
it('should create a value object', () => {
const role1 = RoleName.create(RoleName.NAMES.ProUser).getValue()
const valueOrError = RoleNameCollection.create([role1])
const valueOrError = RoleNameCollection.create([RoleName.NAMES.ProUser])
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual([role1])
expect(valueOrError.getValue().value[0].value).toEqual('PRO_USER')
})
it('should tell if collections are not equal', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
let roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
]).getValue()
let roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser]).getValue()
let valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
RoleName.NAMES.ProUser,
RoleName.NAMES.PlusUser,
RoleName.NAMES.CoreUser,
]).getValue()
valueOrError = RoleNameCollection.create(roles1)
@@ -36,42 +28,30 @@ describe('RoleNameCollection', () => {
})
it('should tell if collections are equal', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
const roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]).getValue()
const roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]).getValue()
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().equals(roles2)).toBeTruthy()
})
it('should tell if collection includes element', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.ProUser).getValue())).toBeTruthy()
})
it('should tell if collection does not includes element', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.CoreUser).getValue())).toBeFalsy()
})
it('should tell if collection has a role with more or equal power to', () => {
let roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue()]
let roles = [RoleName.NAMES.CoreUser]
let valueOrError = RoleNameCollection.create(roles)
let roleNames = valueOrError.getValue()
@@ -83,7 +63,7 @@ describe('RoleNameCollection', () => {
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
roles = [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]
valueOrError = RoleNameCollection.create(roles)
roleNames = valueOrError.getValue()
@@ -95,7 +75,7 @@ describe('RoleNameCollection', () => {
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
roles = [RoleName.create(RoleName.NAMES.ProUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
roles = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
valueOrError = RoleNameCollection.create(roles)
roleNames = valueOrError.getValue()
@@ -109,4 +89,11 @@ describe('RoleNameCollection', () => {
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
})
it('should fail to create a collection if a role name is invalid', () => {
const valueOrError = RoleNameCollection.create(['invalid-role-name'])
expect(valueOrError.isFailed()).toBeTruthy()
expect(valueOrError.getError()).toEqual('Invalid role name: invalid-role-name')
})
})
@@ -46,7 +46,16 @@ export class RoleNameCollection extends ValueObject<RoleNameCollectionProps> {
super(props)
}
static create(roleName: RoleName[]): Result<RoleNameCollection> {
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleName }))
static create(roleNameStrings: string[]): Result<RoleNameCollection> {
const roleNames: RoleName[] = []
for (const roleNameString of roleNameStrings) {
const roleNameOrError = RoleName.create(roleNameString)
if (roleNameOrError.isFailed()) {
return Result.fail<RoleNameCollection>(roleNameOrError.getError())
}
roleNames.push(roleNameOrError.getValue())
}
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleNames }))
}
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.20](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.19...@standardnotes/event-store@1.11.20) (2023-08-18)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.19](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.18...@standardnotes/event-store@1.11.19) (2023-08-09)
**Note:** Version bump only for package @standardnotes/event-store
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.11.19",
"version": "1.11.20",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.3...@standardnotes/files-server@1.20.4) (2023-08-18)
**Note:** Version bump only for package @standardnotes/files-server
## [1.20.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.2...@standardnotes/files-server@1.20.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/files-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.20.3",
"version": "1.20.4",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+9
View File
@@ -9,3 +9,12 @@ PSEUDO_KEY_PARAMS_KEY=
VALET_TOKEN_SECRET=
FILES_SERVER_URL=
SECONDARY_DB_ENABLED=false
MONGO_HOST=localhost
MONGO_PORT=27017
MONGO_USERNAME=standardnotes
MONGO_PASSWORD=standardnotes
MONGO_DATABASE=standardnotes
TRANSITION_MODE_ENABLED=false
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.51...@standardnotes/home-server@1.14.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.13.51](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.50...@standardnotes/home-server@1.13.51) (2023-08-17)
### Bug Fixes
* **home-server:** add default env values for secondary database ([916e989](https://github.com/standardnotes/server/commit/916e98936a276a3960d949c5b70803214c945686))
## [1.13.50](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.49...@standardnotes/home-server@1.13.50) (2023-08-16)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.49](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.48...@standardnotes/home-server@1.13.49) (2023-08-15)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.48](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.47...@standardnotes/home-server@1.13.48) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.47](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.46...@standardnotes/home-server@1.13.47) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.46](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.45...@standardnotes/home-server@1.13.46) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.13.46",
"version": "1.14.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.26.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.7...@standardnotes/revisions-server@1.26.8) (2023-08-18)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.6...@standardnotes/revisions-server@1.26.7) (2023-08-11)
**Note:** Version bump only for package @standardnotes/revisions-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.26.7",
"version": "1.26.8",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.23](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.22...@standardnotes/scheduler-server@1.20.23) (2023-08-18)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.20.22](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.21...@standardnotes/scheduler-server@1.20.22) (2023-08-11)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.20.22",
"version": "1.20.23",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.25](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.24...@standardnotes/settings@1.21.25) (2023-08-18)
**Note:** Version bump only for package @standardnotes/settings
## [1.21.24](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.23...@standardnotes/settings@1.21.24) (2023-08-09)
**Note:** Version bump only for package @standardnotes/settings
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/settings",
"version": "1.21.24",
"version": "1.21.25",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+8
View File
@@ -52,3 +52,11 @@ FILE_UPLOAD_PATH=
VALET_TOKEN_SECRET=change-me-!
VALET_TOKEN_TTL=7200
# (Optional) Mongo Setup
SECONDARY_DB_ENABLED=false
MONGO_HOST=
MONGO_PORT=
MONGO_USERNAME=
MONGO_PASSWORD=
MONGO_DATABASE=
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.80.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.79.1...@standardnotes/syncing-server@1.80.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/syncing-server-js/issues/700)) ([302b624](https://github.com/standardnotes/syncing-server-js/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.79.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.79.0...@standardnotes/syncing-server@1.79.1) (2023-08-17)
### Bug Fixes
* **syncing-server:** refactor shared vault and key system associations ([#698](https://github.com/standardnotes/syncing-server-js/issues/698)) ([31d1eef](https://github.com/standardnotes/syncing-server-js/commit/31d1eef7f74310b176085311fc04c2efc4a7059f))
# [1.79.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.11...@standardnotes/syncing-server@1.79.0) (2023-08-16)
### Features
* add mongodb initial support ([#696](https://github.com/standardnotes/syncing-server-js/issues/696)) ([b24b576](https://github.com/standardnotes/syncing-server-js/commit/b24b5762093c0f48a28dfb879339c1b9927c9333))
## [1.78.11](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.10...@standardnotes/syncing-server@1.78.11) (2023-08-11)
### Reverts
* Revert "tmp: disable shared vaults" ([d02124f](https://github.com/standardnotes/syncing-server-js/commit/d02124f4e505e4f7e7510637c461fdd0552c381a))
* Revert "tmp: disable decorating with associations on revisions" ([ad4b85b](https://github.com/standardnotes/syncing-server-js/commit/ad4b85b095a8539955f7b47d51643121d33eed6a))
* Revert "tmp: disable decorating items completely" ([0bf7d8b](https://github.com/standardnotes/syncing-server-js/commit/0bf7d8beae2de1b86d95711a4e15e84b8bf5e9c0))
## [1.78.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.9...@standardnotes/syncing-server@1.78.10) (2023-08-11)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.78.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.78.8...@standardnotes/syncing-server@1.78.9) (2023-08-11)
**Note:** Version bump only for package @standardnotes/syncing-server
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveRevisionsForeignKey1692176803410 implements MigrationInterface {
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) {
try {
await queryRunner.query('ALTER TABLE `revisions` DROP FOREIGN KEY `FK_ab3b92e54701fe3010022a31d90`')
} catch (error) {
// eslint-disable-next-line no-console
console.log('Error dropping foreign key: ', (error as Error).message)
}
}
}
public async down(): Promise<void> {
return
}
}
@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveAssociations1692264556858 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX `key_system_identifier_on_key_system_associations` ON `key_system_associations`',
)
await queryRunner.query('DROP INDEX `item_uuid_on_key_system_associations` ON `key_system_associations`')
await queryRunner.query('DROP TABLE `key_system_associations`')
await queryRunner.query('DROP INDEX `item_uuid_on_shared_vault_associations` ON `shared_vault_associations`')
await queryRunner.query(
'DROP INDEX `shared_vault_uuid_on_shared_vault_associations` ON `shared_vault_associations`',
)
await queryRunner.query('DROP TABLE `shared_vault_associations`')
}
public async down(): Promise<void> {
return
}
}
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveAssociations1692264735730 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "key_system_identifier_on_key_system_associations"')
await queryRunner.query('DROP INDEX "item_uuid_on_key_system_associations"')
await queryRunner.query('DROP TABLE "key_system_associations"')
await queryRunner.query('DROP INDEX "item_uuid_on_shared_vault_associations"')
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_shared_vault_associations"')
await queryRunner.query('DROP TABLE "shared_vault_associations"')
}
public async down(): Promise<void> {
return
}
}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.78.9",
"version": "1.80.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -49,6 +49,7 @@
"inversify": "^6.0.1",
"inversify-express-utils": "^6.4.3",
"jsonwebtoken": "^9.0.0",
"mongodb": "^5.7.0",
"mysql2": "^3.0.1",
"nodemon": "^2.0.19",
"prettyjson": "^1.2.5",
+129 -129
View File
@@ -7,7 +7,7 @@ import { AppDataSource } from './DataSource'
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
import { TypeORMItemRepository } from '../Infra/TypeORM/TypeORMItemRepository'
import { Repository } from 'typeorm'
import { MongoRepository, Repository } from 'typeorm'
import { Item } from '../Domain/Item/Item'
import {
DirectCallDomainEventPublisher,
@@ -39,7 +39,7 @@ import { SyncItems } from '../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { InversifyExpressAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware'
import { S3Client } from '@aws-sdk/client-s3'
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
import { ContentDecoder } from '@standardnotes/common'
import { ContentDecoder, ContentDecoderInterface } from '@standardnotes/common'
import {
DomainEventMessageHandlerInterface,
DomainEventHandlerInterface,
@@ -79,15 +79,7 @@ import { ItemHashHttpMapper } from '../Mapping/Http/ItemHashHttpMapper'
import { ItemHash } from '../Domain/Item/ItemHash'
import { ItemHashHttpRepresentation } from '../Mapping/Http/ItemHashHttpRepresentation'
import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
import { SharedVaultAssociation } from '../Domain/SharedVault/SharedVaultAssociation'
import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
import { SharedVaultAssociationPersistenceMapper } from '../Mapping/Persistence/SharedVaultAssociationPersistenceMapper'
import { TypeORMKeySystemAssociationRepository } from '../Infra/TypeORM/TypeORMKeySystemAssociationRepository'
import { SharedVaultAssociationRepositoryInterface } from '../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
import { TypeORMSharedVaultAssociationRepository } from '../Infra/TypeORM/TypeORMSharedVaultAssociationRepository'
import { KeySystemAssociation } from '../Domain/KeySystem/KeySystemAssociation'
import { KeySystemAssociationRepositoryInterface } from '../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
import { KeySystemAssociationPersistenceMapper } from '../Mapping/Persistence/KeySystemAssociationPersistenceMapper'
import { BaseSharedVaultInvitesController } from '../Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController'
import { InviteUserToSharedVault } from '../Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault'
import { TypeORMSharedVaultRepository } from '../Infra/TypeORM/TypeORMSharedVaultRepository'
@@ -158,6 +150,12 @@ import { UpdateStorageQuotaUsedInSharedVault } from '../Domain/UseCase/SharedVau
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
import { AddNotificationsForUsers } from '../Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers'
import { MongoDBItem } from '../Infra/TypeORM/MongoDBItem'
import { MongoDBItemRepository } from '../Infra/TypeORM/MongoDBItemRepository'
import { MongoDBItemPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBItemPersistenceMapper'
import { Logger } from 'winston'
import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -210,6 +208,7 @@ export class ContainerConfigLoader {
container.bind<TimerInterface>(TYPES.Sync_Timer).toConstantValue(new Timer())
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
const isSecondaryDatabaseEnabled = env.get('SECONDARY_DB_ENABLED', true) === 'true'
container.bind<Env>(TYPES.Sync_Env).toConstantValue(env)
@@ -288,6 +287,18 @@ export class ContainerConfigLoader {
})
}
container
.bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
.toConstantValue(
env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) ? +env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) : 10485760,
)
container.bind(TYPES.Sync_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container
.bind(TYPES.Sync_FILE_UPLOAD_PATH)
.toConstantValue(
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
)
// Mapping
container
.bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
@@ -312,16 +323,6 @@ export class ContainerConfigLoader {
container
.bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
.toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
container
.bind<MapperInterface<KeySystemAssociation, TypeORMKeySystemAssociation>>(
TYPES.Sync_KeySystemAssociationPersistenceMapper,
)
.toConstantValue(new KeySystemAssociationPersistenceMapper())
container
.bind<MapperInterface<SharedVaultAssociation, TypeORMSharedVaultAssociation>>(
TYPES.Sync_SharedVaultAssociationPersistenceMapper,
)
.toConstantValue(new SharedVaultAssociationPersistenceMapper())
container
.bind<MapperInterface<SharedVault, TypeORMSharedVault>>(TYPES.Sync_SharedVaultPersistenceMapper)
.toConstantValue(new SharedVaultPersistenceMapper())
@@ -381,32 +382,43 @@ export class ContainerConfigLoader {
.bind<Repository<TypeORMMessage>>(TYPES.Sync_ORMMessageRepository)
.toConstantValue(appDataSource.getRepository(TypeORMMessage))
// Mongo
if (isSecondaryDatabaseEnabled) {
container
.bind<MapperInterface<Item, MongoDBItem>>(TYPES.Sync_MongoDBItemPersistenceMapper)
.toConstantValue(new MongoDBItemPersistenceMapper())
container
.bind<MongoRepository<MongoDBItem>>(TYPES.Sync_ORMMongoItemRepository)
.toConstantValue(appDataSource.getMongoRepository(MongoDBItem))
container
.bind<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
.toConstantValue(
new MongoDBItemRepository(
container.get<MongoRepository<MongoDBItem>>(TYPES.Sync_ORMMongoItemRepository),
container.get<MapperInterface<Item, MongoDBItem>>(TYPES.Sync_MongoDBItemPersistenceMapper),
container.get<Logger>(TYPES.Sync_Logger),
),
)
}
// Repositories
container
.bind<KeySystemAssociationRepositoryInterface>(TYPES.Sync_KeySystemAssociationRepository)
.toConstantValue(
new TypeORMKeySystemAssociationRepository(
container.get(TYPES.Sync_ORMKeySystemAssociationRepository),
container.get(TYPES.Sync_KeySystemAssociationPersistenceMapper),
),
)
container
.bind<SharedVaultAssociationRepositoryInterface>(TYPES.Sync_SharedVaultAssociationRepository)
.toConstantValue(
new TypeORMSharedVaultAssociationRepository(
container.get(TYPES.Sync_ORMSharedVaultAssociationRepository),
container.get(TYPES.Sync_SharedVaultAssociationPersistenceMapper),
),
)
container
.bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository)
.bind<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository)
.toConstantValue(
new TypeORMItemRepository(
container.get(TYPES.Sync_ORMItemRepository),
container.get(TYPES.Sync_ItemPersistenceMapper),
container.get(TYPES.Sync_KeySystemAssociationRepository),
container.get(TYPES.Sync_SharedVaultAssociationRepository),
container.get(TYPES.Sync_Logger),
container.get<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository),
container.get<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver)
.toConstantValue(
new TypeORMItemRepositoryResolver(
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
),
)
container
@@ -459,10 +471,7 @@ export class ContainerConfigLoader {
container
.bind<ItemTransferCalculatorInterface>(TYPES.Sync_ItemTransferCalculator)
.toDynamicValue((context: interfaces.Context) => {
return new ItemTransferCalculator(
context.container.get(TYPES.Sync_ItemRepository),
context.container.get(TYPES.Sync_Logger),
)
return new ItemTransferCalculator(context.container.get<Logger>(TYPES.Sync_Logger))
})
// Middleware
@@ -540,7 +549,8 @@ export class ContainerConfigLoader {
.bind<GetItems>(TYPES.Sync_GetItems)
.toConstantValue(
new GetItems(
container.get(TYPES.Sync_ItemRepository),
container.get(TYPES.Sync_ItemRepositoryResolver),
container.get(TYPES.Sync_SharedVaultUserRepository),
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
container.get(TYPES.Sync_ItemTransferCalculator),
container.get(TYPES.Sync_Timer),
@@ -551,7 +561,7 @@ export class ContainerConfigLoader {
.bind<SaveNewItem>(TYPES.Sync_SaveNewItem)
.toConstantValue(
new SaveNewItem(
container.get(TYPES.Sync_ItemRepository),
container.get(TYPES.Sync_ItemRepositoryResolver),
container.get(TYPES.Sync_Timer),
container.get(TYPES.Sync_DomainEventPublisher),
container.get(TYPES.Sync_DomainEventFactory),
@@ -577,7 +587,7 @@ export class ContainerConfigLoader {
.bind<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem)
.toConstantValue(
new UpdateExistingItem(
container.get(TYPES.Sync_ItemRepository),
container.get(TYPES.Sync_ItemRepositoryResolver),
container.get(TYPES.Sync_Timer),
container.get(TYPES.Sync_DomainEventPublisher),
container.get(TYPES.Sync_DomainEventFactory),
@@ -592,7 +602,7 @@ export class ContainerConfigLoader {
.toConstantValue(
new SaveItems(
container.get(TYPES.Sync_ItemSaveValidator),
container.get(TYPES.Sync_ItemRepository),
container.get(TYPES.Sync_ItemRepositoryResolver),
container.get(TYPES.Sync_Timer),
container.get(TYPES.Sync_SaveNewItem),
container.get(TYPES.Sync_UpdateExistingItem),
@@ -621,7 +631,7 @@ export class ContainerConfigLoader {
.bind<SyncItems>(TYPES.Sync_SyncItems)
.toConstantValue(
new SyncItems(
container.get(TYPES.Sync_ItemRepository),
container.get(TYPES.Sync_ItemRepositoryResolver),
container.get(TYPES.Sync_GetItems),
container.get(TYPES.Sync_SaveItems),
container.get(TYPES.Sync_GetSharedVaults),
@@ -631,10 +641,10 @@ export class ContainerConfigLoader {
),
)
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepository))
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepositoryResolver))
})
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
return new GetItem(context.container.get(TYPES.Sync_ItemRepositoryResolver))
})
container
.bind<InviteUserToSharedVault>(TYPES.Sync_InviteUserToSharedVault)
@@ -792,48 +802,56 @@ export class ContainerConfigLoader {
)
})
// env vars
container
.bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
.toConstantValue(
env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) ? +env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) : 10485760,
)
container.bind(TYPES.Sync_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container
.bind(TYPES.Sync_FILE_UPLOAD_PATH)
.toConstantValue(
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
env.get('S3_AWS_REGION', true)
? new S3ItemBackupService(
container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
container.get(TYPES.Sync_ItemBackupMapper),
container.get(TYPES.Sync_ItemHttpMapper),
container.get(TYPES.Sync_Logger),
container.get(TYPES.Sync_S3),
)
: new FSItemBackupService(
container.get(TYPES.Sync_FILE_UPLOAD_PATH),
container.get(TYPES.Sync_ItemBackupMapper),
container.get(TYPES.Sync_Logger),
),
)
// Handlers
container
.bind<DuplicateItemSyncedEventHandler>(TYPES.Sync_DuplicateItemSyncedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new DuplicateItemSyncedEventHandler(
context.container.get(TYPES.Sync_ItemRepository),
context.container.get(TYPES.Sync_DomainEventFactory),
context.container.get(TYPES.Sync_DomainEventPublisher),
context.container.get(TYPES.Sync_Logger),
)
})
.toConstantValue(
new DuplicateItemSyncedEventHandler(
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<AccountDeletionRequestedEventHandler>(TYPES.Sync_AccountDeletionRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new AccountDeletionRequestedEventHandler(
context.container.get(TYPES.Sync_ItemRepository),
context.container.get(TYPES.Sync_Logger),
)
})
.toConstantValue(
new AccountDeletionRequestedEventHandler(
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new ItemRevisionCreationRequestedEventHandler(
context.container.get(TYPES.Sync_ItemRepository),
context.container.get(TYPES.Sync_ItemBackupService),
context.container.get(TYPES.Sync_DomainEventFactory),
context.container.get(TYPES.Sync_DomainEventPublisher),
)
})
.toConstantValue(
new ItemRevisionCreationRequestedEventHandler(
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
),
)
container
.bind<SharedVaultFileUploadedEventHandler>(TYPES.Sync_SharedVaultFileUploadedEventHandler)
.toConstantValue(
@@ -858,38 +876,17 @@ export class ContainerConfigLoader {
container.bind<AxiosInstance>(TYPES.Sync_HTTPClient).toDynamicValue(() => axios.create())
container
.bind<ExtensionsHttpServiceInterface>(TYPES.Sync_ExtensionsHttpService)
.toDynamicValue((context: interfaces.Context) => {
return new ExtensionsHttpService(
context.container.get(TYPES.Sync_HTTPClient),
context.container.get(TYPES.Sync_ItemRepository),
context.container.get(TYPES.Sync_ContentDecoder),
context.container.get(TYPES.Sync_DomainEventPublisher),
context.container.get(TYPES.Sync_DomainEventFactory),
context.container.get(TYPES.Sync_Logger),
)
})
container
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
.toDynamicValue((context: interfaces.Context) => {
const env: Env = context.container.get(TYPES.Sync_Env)
if (env.get('S3_AWS_REGION', true)) {
return new S3ItemBackupService(
context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
context.container.get(TYPES.Sync_ItemBackupMapper),
context.container.get(TYPES.Sync_ItemHttpMapper),
context.container.get(TYPES.Sync_Logger),
context.container.get(TYPES.Sync_S3),
)
} else {
return new FSItemBackupService(
context.container.get(TYPES.Sync_FILE_UPLOAD_PATH),
context.container.get(TYPES.Sync_ItemBackupMapper),
context.container.get(TYPES.Sync_Logger),
)
}
})
.toConstantValue(
new ExtensionsHttpService(
container.get<AxiosInstance>(TYPES.Sync_HTTPClient),
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
container.get<ContentDecoderInterface>(TYPES.Sync_ContentDecoder),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<Logger>(TYPES.Sync_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['DUPLICATE_ITEM_SYNCED', container.get(TYPES.Sync_DuplicateItemSyncedEventHandler)],
@@ -918,19 +915,22 @@ export class ContainerConfigLoader {
container
.bind<EmailBackupRequestedEventHandler>(TYPES.Sync_EmailBackupRequestedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new EmailBackupRequestedEventHandler(
context.container.get(TYPES.Sync_ItemRepository),
context.container.get(TYPES.Sync_AuthHttpService),
context.container.get(TYPES.Sync_ItemBackupService),
context.container.get(TYPES.Sync_DomainEventPublisher),
context.container.get(TYPES.Sync_DomainEventFactory),
context.container.get(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE),
context.container.get(TYPES.Sync_ItemTransferCalculator),
context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
context.container.get(TYPES.Sync_Logger),
)
})
.toConstantValue(
new EmailBackupRequestedEventHandler(
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
isSecondaryDatabaseEnabled
? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
: null,
container.get<AuthHttpServiceInterface>(TYPES.Sync_AuthHttpService),
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<number>(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE),
container.get<ItemTransferCalculatorInterface>(TYPES.Sync_ItemTransferCalculator),
container.get<string>(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
container.get<Logger>(TYPES.Sync_Logger),
),
)
eventHandlers.set('EMAIL_BACKUP_REQUESTED', container.get(TYPES.Sync_EmailBackupRequestedEventHandler))
}
@@ -1,4 +1,4 @@
import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } from 'typeorm'
import { DataSource, EntityTarget, LoggerOptions, MongoRepository, ObjectLiteral, Repository } from 'typeorm'
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
import { Env } from './Env'
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
@@ -10,9 +10,11 @@ import { TypeORMSharedVault } from '../Infra/TypeORM/TypeORMSharedVault'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
import { TypeORMSharedVaultInvite } from '../Infra/TypeORM/TypeORMSharedVaultInvite'
import { TypeORMMessage } from '../Infra/TypeORM/TypeORMMessage'
import { MongoDBItem } from '../Infra/TypeORM/MongoDBItem'
export class AppDataSource {
private _dataSource: DataSource | undefined
private _secondaryDataSource: DataSource | undefined
constructor(private env: Env) {}
@@ -24,8 +26,42 @@ export class AppDataSource {
return this._dataSource.getRepository(target)
}
getMongoRepository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): MongoRepository<Entity> {
if (!this._secondaryDataSource) {
throw new Error('Secondary DataSource not initialized')
}
return this._secondaryDataSource.getMongoRepository(target)
}
async initialize(): Promise<void> {
await this.dataSource.initialize()
const secondaryDataSource = this.secondaryDataSource
if (secondaryDataSource) {
await secondaryDataSource.initialize()
}
}
get secondaryDataSource(): DataSource | undefined {
this.env.load()
if (this.env.get('SECONDARY_DB_ENABLED', true) !== 'true') {
return undefined
}
this._secondaryDataSource = new DataSource({
type: 'mongodb',
host: this.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'),
entities: [MongoDBItem],
synchronize: true,
})
return this._secondaryDataSource
}
get dataSource(): DataSource {
@@ -7,9 +7,9 @@ const TYPES = {
Sync_S3: Symbol.for('Sync_S3'),
Sync_Env: Symbol.for('Sync_Env'),
// Repositories
Sync_ItemRepository: Symbol.for('Sync_ItemRepository'),
Sync_KeySystemAssociationRepository: Symbol.for('Sync_KeySystemAssociationRepository'),
Sync_SharedVaultAssociationRepository: Symbol.for('Sync_SharedVaultAssociationRepository'),
Sync_ItemRepositoryResolver: Symbol.for('Sync_ItemRepositoryResolver'),
Sync_MySQLItemRepository: Symbol.for('Sync_MySQLItemRepository'),
Sync_MongoDBItemRepository: Symbol.for('Sync_MongoDBItemRepository'),
Sync_SharedVaultRepository: Symbol.for('Sync_SharedVaultRepository'),
Sync_SharedVaultInviteRepository: Symbol.for('Sync_SharedVaultInviteRepository'),
Sync_SharedVaultUserRepository: Symbol.for('Sync_SharedVaultUserRepository'),
@@ -24,6 +24,8 @@ const TYPES = {
Sync_ORMSharedVaultUserRepository: Symbol.for('Sync_ORMSharedVaultUserRepository'),
Sync_ORMNotificationRepository: Symbol.for('Sync_ORMNotificationRepository'),
Sync_ORMMessageRepository: Symbol.for('Sync_ORMMessageRepository'),
// Mongo
Sync_ORMMongoItemRepository: Symbol.for('Sync_ORMMongoItemRepository'),
// Middleware
Sync_AuthMiddleware: Symbol.for('Sync_AuthMiddleware'),
// env vars
@@ -124,13 +126,12 @@ const TYPES = {
Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
Sync_NotificationHttpMapper: Symbol.for('Sync_NotificationHttpMapper'),
Sync_ItemPersistenceMapper: Symbol.for('Sync_ItemPersistenceMapper'),
Sync_MongoDBItemPersistenceMapper: Symbol.for('Sync_MongoDBItemPersistenceMapper'),
Sync_ItemHttpMapper: Symbol.for('Sync_ItemHttpMapper'),
Sync_ItemHashHttpMapper: Symbol.for('Sync_ItemHashHttpMapper'),
Sync_SavedItemHttpMapper: Symbol.for('Sync_SavedItemHttpMapper'),
Sync_ItemConflictHttpMapper: Symbol.for('Sync_ItemConflictHttpMapper'),
Sync_ItemBackupMapper: Symbol.for('Sync_ItemBackupMapper'),
Sync_KeySystemAssociationPersistenceMapper: Symbol.for('Sync_KeySystemAssociationPersistenceMapper'),
Sync_SharedVaultAssociationPersistenceMapper: Symbol.for('Sync_SharedVaultAssociationPersistenceMapper'),
Sync_SharedVaultPersistenceMapper: Symbol.for('Sync_SharedVaultPersistenceMapper'),
Sync_SharedVaultUserPersistenceMapper: Symbol.for('Sync_SharedVaultUserPersistenceMapper'),
Sync_SharedVaultInvitePersistenceMapper: Symbol.for('Sync_SharedVaultInvitePersistenceMapper'),
@@ -13,7 +13,8 @@ import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardn
describe('ExtensionsHttpService', () => {
let httpClient: AxiosInstance
let itemRepository: ItemRepositoryInterface
let primaryItemRepository: ItemRepositoryInterface
let secondaryItemRepository: ItemRepositoryInterface | null
let contentDecoder: ContentDecoderInterface
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
@@ -24,7 +25,8 @@ describe('ExtensionsHttpService', () => {
const createService = () =>
new ExtensionsHttpService(
httpClient,
itemRepository,
primaryItemRepository,
secondaryItemRepository,
contentDecoder,
domainEventPublisher,
domainEventFactory,
@@ -54,8 +56,8 @@ describe('ExtensionsHttpService', () => {
authParams = {} as jest.Mocked<KeyParamsData>
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
@@ -191,6 +193,31 @@ describe('ExtensionsHttpService', () => {
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed backup event if the extension is in the secondary repository', async () => {
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
secondaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: '',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
secondaryItemRepository = null
})
it('should publish a failed Dropbox backup event if request was sent and extensions server responded not ok', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
@@ -273,7 +300,7 @@ describe('ExtensionsHttpService', () => {
})
it('should throw an error if the extension to post to is not found', async () => {
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
@@ -299,7 +326,7 @@ describe('ExtensionsHttpService', () => {
it('should throw an error if the extension to post to has no content', async () => {
item = {} as jest.Mocked<Item>
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
@@ -17,7 +17,8 @@ import { getBody as oneDriveBody, getSubject as oneDriveSubject } from '../Email
export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
constructor(
private httpClient: AxiosInstance,
private itemRepository: ItemRepositoryInterface,
private primaryItemRepository: ItemRepositoryInterface,
private secondaryItemRepository: ItemRepositoryInterface | null,
private contentDecoder: ContentDecoderInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
@@ -139,9 +140,14 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
userUuid: string,
email: string,
): Promise<DomainEventInterface> {
const extension = await this.itemRepository.findByUuidAndUserUuid(extensionId, userUuid)
let extension = await this.primaryItemRepository.findByUuidAndUserUuid(extensionId, userUuid)
if (extension === null || !extension.props.content) {
throw Error(`Could not find extensions with id ${extensionId}`)
if (this.secondaryItemRepository) {
extension = await this.secondaryItemRepository.findByUuidAndUserUuid(extensionId, userUuid)
}
if (extension === null || !extension.props.content) {
throw Error(`Could not find extensions with id ${extensionId}`)
}
}
const content = this.contentDecoder.decode(extension.props.content)
@@ -8,12 +8,14 @@ import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequested
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
describe('AccountDeletionRequestedEventHandler', () => {
let itemRepository: ItemRepositoryInterface
let primaryItemRepository: ItemRepositoryInterface
let secondaryItemRepository: ItemRepositoryInterface | null
let logger: Logger
let event: AccountDeletionRequestedEvent
let item: Item
const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepository, logger)
const createHandler = () =>
new AccountDeletionRequestedEventHandler(primaryItemRepository, secondaryItemRepository, logger)
beforeEach(() => {
item = Item.create(
@@ -33,9 +35,9 @@ describe('AccountDeletionRequestedEventHandler', () => {
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findAll = jest.fn().mockReturnValue([item])
itemRepository.deleteByUserUuid = jest.fn()
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
primaryItemRepository.deleteByUserUuid = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
@@ -52,6 +54,17 @@ describe('AccountDeletionRequestedEventHandler', () => {
it('should remove all items for a user', async () => {
await createHandler().handle(event)
expect(itemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
expect(primaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
})
it('should remove all items for a user from secondary repository', async () => {
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
secondaryItemRepository.deleteByUserUuid = jest.fn()
await createHandler().handle(event)
expect(secondaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
secondaryItemRepository = null
})
})
@@ -3,10 +3,17 @@ import { Logger } from 'winston'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
constructor(
private primaryItemRepository: ItemRepositoryInterface,
private secondaryItemRepository: ItemRepositoryInterface | null,
private logger: Logger,
) {}
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
await this.itemRepository.deleteByUserUuid(event.payload.userUuid)
await this.primaryItemRepository.deleteByUserUuid(event.payload.userUuid)
if (this.secondaryItemRepository) {
await this.secondaryItemRepository.deleteByUserUuid(event.payload.userUuid)
}
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
}
@@ -13,7 +13,8 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
describe('DuplicateItemSyncedEventHandler', () => {
let itemRepository: ItemRepositoryInterface
let primaryItemRepository: ItemRepositoryInterface
let secondaryItemRepository: ItemRepositoryInterface | null
let logger: Logger
let duplicateItem: Item
let originalItem: Item
@@ -22,7 +23,13 @@ describe('DuplicateItemSyncedEventHandler', () => {
let domainEventPublisher: DomainEventPublisherInterface
const createHandler = () =>
new DuplicateItemSyncedEventHandler(itemRepository, domainEventFactory, domainEventPublisher, logger)
new DuplicateItemSyncedEventHandler(
primaryItemRepository,
secondaryItemRepository,
domainEventFactory,
domainEventPublisher,
logger,
)
beforeEach(() => {
originalItem = Item.create(
@@ -59,8 +66,8 @@ describe('DuplicateItemSyncedEventHandler', () => {
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
).getValue()
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findByUuidAndUserUuid = jest
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
primaryItemRepository.findByUuidAndUserUuid = jest
.fn()
.mockReturnValueOnce(duplicateItem)
.mockReturnValueOnce(originalItem)
@@ -90,8 +97,22 @@ describe('DuplicateItemSyncedEventHandler', () => {
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should copy revisions from original item to the duplicate item in the secondary repository', async () => {
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
secondaryItemRepository.findByUuidAndUserUuid = jest
.fn()
.mockReturnValueOnce(duplicateItem)
.mockReturnValueOnce(originalItem)
await createHandler().handle(event)
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
secondaryItemRepository = null
})
it('should not copy revisions if original item does not exist', async () => {
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
await createHandler().handle(event)
@@ -99,7 +120,7 @@ describe('DuplicateItemSyncedEventHandler', () => {
})
it('should not copy revisions if duplicate item does not exist', async () => {
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
await createHandler().handle(event)
@@ -9,14 +9,26 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterface {
constructor(
private itemRepository: ItemRepositoryInterface,
private primaryItemRepository: ItemRepositoryInterface,
private secondaryItemRepository: ItemRepositoryInterface | null,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private logger: Logger,
) {}
async handle(event: DuplicateItemSyncedEvent): Promise<void> {
const item = await this.itemRepository.findByUuidAndUserUuid(event.payload.itemUuid, event.payload.userUuid)
await this.requestRevisionsCopy(event, this.primaryItemRepository)
if (this.secondaryItemRepository) {
await this.requestRevisionsCopy(event, this.secondaryItemRepository)
}
}
private async requestRevisionsCopy(
event: DuplicateItemSyncedEvent,
itemRepository: ItemRepositoryInterface,
): Promise<void> {
const item = await itemRepository.findByUuidAndUserUuid(event.payload.itemUuid, event.payload.userUuid)
if (item === null) {
this.logger.warn(`Could not find item with uuid ${event.payload.itemUuid}`)
@@ -30,7 +42,7 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
return
}
const existingOriginalItem = await this.itemRepository.findByUuidAndUserUuid(
const existingOriginalItem = await itemRepository.findByUuidAndUserUuid(
item.props.duplicateOf.value,
event.payload.userUuid,
)
@@ -13,9 +13,11 @@ import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { EmailBackupRequestedEventHandler } from './EmailBackupRequestedEventHandler'
import { ItemTransferCalculatorInterface } from '../Item/ItemTransferCalculatorInterface'
import { ItemContentSizeDescriptor } from '../Item/ItemContentSizeDescriptor'
describe('EmailBackupRequestedEventHandler', () => {
let itemRepository: ItemRepositoryInterface
let primaryItemRepository: ItemRepositoryInterface
let secondaryItemRepository: ItemRepositoryInterface | null
let authHttpService: AuthHttpServiceInterface
let itemBackupService: ItemBackupServiceInterface
let domainEventPublisher: DomainEventPublisherInterface
@@ -28,7 +30,8 @@ describe('EmailBackupRequestedEventHandler', () => {
const createHandler = () =>
new EmailBackupRequestedEventHandler(
itemRepository,
primaryItemRepository,
secondaryItemRepository,
authHttpService,
itemBackupService,
domainEventPublisher,
@@ -42,8 +45,11 @@ describe('EmailBackupRequestedEventHandler', () => {
beforeEach(() => {
item = {} as jest.Mocked<Item>
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findAll = jest.fn().mockReturnValue([item])
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
primaryItemRepository.findContentSizeForComputingTransferLimit = jest
.fn()
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
authHttpService = {} as jest.Mocked<AuthHttpServiceInterface>
authHttpService.getUserKeyParams = jest.fn().mockReturnValue({ identifier: 'test@test.com' })
@@ -81,6 +87,21 @@ describe('EmailBackupRequestedEventHandler', () => {
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should inform that backup attachment for email was created in the secondary repository', async () => {
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
secondaryItemRepository.findAll = jest.fn().mockReturnValue([item])
secondaryItemRepository.findContentSizeForComputingTransferLimit = jest
.fn()
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
await createHandler().handle(event)
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalledTimes(2)
secondaryItemRepository = null
})
it('should inform that multipart backup attachment for email was created', async () => {
itemBackupService.backup = jest
.fn()
@@ -16,7 +16,8 @@ import { getBody, getSubject } from '../Email/EmailBackupAttachmentCreated'
export class EmailBackupRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
private itemRepository: ItemRepositoryInterface,
private primaryItemRepository: ItemRepositoryInterface,
private secondaryItemRepository: ItemRepositoryInterface | null,
private authHttpService: AuthHttpServiceInterface,
private itemBackupService: ItemBackupServiceInterface,
private domainEventPublisher: DomainEventPublisherInterface,
@@ -28,6 +29,17 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
) {}
async handle(event: EmailBackupRequestedEvent): Promise<void> {
await this.requestEmailWithBackupFile(event, this.primaryItemRepository)
if (this.secondaryItemRepository) {
await this.requestEmailWithBackupFile(event, this.secondaryItemRepository)
}
}
private async requestEmailWithBackupFile(
event: EmailBackupRequestedEvent,
itemRepository: ItemRepositoryInterface,
): Promise<void> {
let authParams: KeyParamsData
try {
authParams = await this.authHttpService.getUserKeyParams({
@@ -46,14 +58,15 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
sortOrder: 'ASC',
deleted: false,
}
const itemContentSizeDescriptors = await itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
const itemUuidBundles = await this.itemTransferCalculator.computeItemUuidBundlesToFetch(
itemQuery,
itemContentSizeDescriptors,
this.emailAttachmentMaxByteSize,
)
const backupFileNames: string[] = []
for (const itemUuidBundle of itemUuidBundles) {
const items = await this.itemRepository.findAll({
const items = await itemRepository.findAll({
uuids: itemUuidBundle,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
@@ -14,7 +14,8 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
describe('ItemRevisionCreationRequestedEventHandler', () => {
let itemRepository: ItemRepositoryInterface
let primaryItemRepository: ItemRepositoryInterface
let secondaryItemRepository: ItemRepositoryInterface | null
let event: ItemRevisionCreationRequestedEvent
let item: Item
let itemBackupService: ItemBackupServiceInterface
@@ -23,7 +24,8 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
const createHandler = () =>
new ItemRevisionCreationRequestedEventHandler(
itemRepository,
primaryItemRepository,
secondaryItemRepository,
itemBackupService,
domainEventFactory,
domainEventPublisher,
@@ -47,8 +49,8 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findByUuid = jest.fn().mockReturnValue(item)
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
primaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
event = {} as jest.Mocked<ItemRevisionCreationRequestedEvent>
event.createdAt = new Date(1)
@@ -80,8 +82,20 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
})
it('should create a revision for an item in the secondary repository', async () => {
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
secondaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
await createHandler().handle(event)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
secondaryItemRepository = null
})
it('should not create a revision for an item that does not exist', async () => {
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
primaryItemRepository.findByUuid = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
@@ -11,20 +11,32 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
export class ItemRevisionCreationRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
private itemRepository: ItemRepositoryInterface,
private primaryItemRepository: ItemRepositoryInterface,
private secondaryItemRepository: ItemRepositoryInterface | null,
private itemBackupService: ItemBackupServiceInterface,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
) {}
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
await this.createItemDump(event, this.primaryItemRepository)
if (this.secondaryItemRepository) {
await this.createItemDump(event, this.secondaryItemRepository)
}
}
private async createItemDump(
event: ItemRevisionCreationRequestedEvent,
itemRepository: ItemRepositoryInterface,
): Promise<void> {
const itemUuidOrError = Uuid.create(event.payload.itemUuid)
if (itemUuidOrError.isFailed()) {
return
}
const itemUuid = itemUuidOrError.getValue()
const item = await this.itemRepository.findByUuid(itemUuid, true)
const item = await itemRepository.findByUuid(itemUuid)
if (item === null) {
return
}
@@ -61,10 +61,8 @@ describe('Item', () => {
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
})
@@ -113,9 +111,7 @@ describe('Item', () => {
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
keySystemAssociation: KeySystemAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
keySystemIdentifier: 'key-system-identifier',
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
})
@@ -144,10 +140,8 @@ describe('Item', () => {
it('should set shared vault association', () => {
const sharedVaultAssociation = SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const entity = Item.create({
@@ -184,10 +178,8 @@ describe('Item', () => {
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
@@ -199,9 +191,7 @@ describe('Item', () => {
it('should set key system association', () => {
const keySystemAssociation = KeySystemAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
keySystemIdentifier: 'key-system-identifier',
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const entity = Item.create({
@@ -238,9 +228,7 @@ describe('Item', () => {
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
keySystemAssociation: KeySystemAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
keySystemIdentifier: 'key-system-identifier',
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
@@ -0,0 +1,15 @@
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
describe('ItemContentSizeDescriptor', () => {
it('should create a value object', () => {
const valueOrError = ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20)
expect(valueOrError.isFailed()).toBeFalsy()
})
it('should return error if shared vault uuid is not valid', () => {
const valueOrError = ItemContentSizeDescriptor.create('invalid', 20)
expect(valueOrError.isFailed()).toBeTruthy()
})
})
@@ -0,0 +1,24 @@
import { Result, Uuid, ValueObject } from '@standardnotes/domain-core'
import { ItemContentSizeDescriptorProps } from './ItemContentSizeDescriptorProps'
export class ItemContentSizeDescriptor extends ValueObject<ItemContentSizeDescriptorProps> {
private constructor(props: ItemContentSizeDescriptorProps) {
super(props)
}
static create(itemUuidString: string, contentSize: number | null): Result<ItemContentSizeDescriptor> {
const uuidOrError = Uuid.create(itemUuidString)
if (uuidOrError.isFailed()) {
return Result.fail<ItemContentSizeDescriptor>(uuidOrError.getError())
}
const uuid = uuidOrError.getValue()
return Result.ok<ItemContentSizeDescriptor>(
new ItemContentSizeDescriptor({
uuid,
contentSize,
}),
)
}
}
@@ -0,0 +1,6 @@
import { Uuid } from '@standardnotes/domain-core'
export interface ItemContentSizeDescriptorProps {
uuid: Uuid
contentSize: number | null
}
@@ -10,7 +10,6 @@ export type ItemQuery = {
offset?: number
limit?: number
createdBetween?: Date[]
selectString?: string
includeSharedVaultUuids?: string[]
exclusiveSharedVaultUuids?: string[]
}
@@ -1,23 +1,19 @@
import { Uuid } from '@standardnotes/domain-core'
import { ReadStream } from 'fs'
import { Item } from './Item'
import { ItemQuery } from './ItemQuery'
import { ExtendedIntegrityPayload } from './ExtendedIntegrityPayload'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
export interface ItemRepositoryInterface {
deleteByUserUuid(userUuid: string): Promise<void>
findAll(query: ItemQuery): Promise<Item[]>
findAllRaw<T>(query: ItemQuery): Promise<T[]>
streamAll(query: ItemQuery): Promise<ReadStream>
countAll(query: ItemQuery): Promise<number>
findContentSizeForComputingTransferLimit(
query: ItemQuery,
): Promise<Array<{ uuid: string; contentSize: number | null }>>
findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<Array<ItemContentSizeDescriptor>>
findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>>
findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]>
findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null>
findByUuid(uuid: Uuid, noAssociations: boolean): Promise<Item | null>
findByUuid(uuid: Uuid): Promise<Item | null>
remove(item: Item): Promise<void>
save(item: Item): Promise<void>
markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void>
@@ -0,0 +1,7 @@
import { RoleNameCollection } from '@standardnotes/domain-core'
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
export interface ItemRepositoryResolverInterface {
resolve(roleNames: RoleNameCollection): ItemRepositoryInterface
}
@@ -1,201 +1,143 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { ItemQuery } from './ItemQuery'
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
import { ItemTransferCalculator } from './ItemTransferCalculator'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
describe('ItemTransferCalculator', () => {
let itemRepository: ItemRepositoryInterface
let logger: Logger
const createCalculator = () => new ItemTransferCalculator(itemRepository, logger)
const createCalculator = () => new ItemTransferCalculator(logger)
beforeEach(() => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([])
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
describe('fetching uuids', () => {
it('should compute uuids to fetch based on transfer limit - one item overlaping limit', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 20,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
contentSize: 20,
},
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
expect(result).toEqual([
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000002',
])
const result = await createCalculator().computeItemUuidsToFetch(query, 50)
expect(result).toEqual(['1-2-3', '2-3-4', '3-4-5'])
})
it('should compute uuids to fetch based on transfer limit - exact limit fit', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 20,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
contentSize: 20,
},
])
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(query, 40)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
expect(result).toEqual(['1-2-3', '2-3-4'])
expect(result).toEqual(['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'])
})
it('should compute uuids to fetch based on transfer limit - content size not defined on an item', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 20,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
},
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
expect(result).toEqual([
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000002',
])
const result = await createCalculator().computeItemUuidsToFetch(query, 50)
expect(result).toEqual(['1-2-3', '2-3-4', '3-4-5'])
})
it('should compute uuids to fetch based on transfer limit - first item over the limit', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 50,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
contentSize: 20,
},
])
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 50).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(query, 40)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
expect(result).toEqual(['1-2-3', '2-3-4'])
expect(result).toEqual(['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'])
})
})
describe('fetching bundles', () => {
it('should compute uuid bundles to fetch based on transfer limit - one item overlaping limit', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 20,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
contentSize: 20,
},
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
expect(result).toEqual([
[
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000002',
],
])
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 50)
expect(result).toEqual([['1-2-3', '2-3-4', '3-4-5']])
})
it('should compute uuid bundles to fetch based on transfer limit - exact limit fit', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 20,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
contentSize: 20,
},
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
expect(result).toEqual([
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
['00000000-0000-0000-0000-000000000002'],
])
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 40)
expect(result).toEqual([['1-2-3', '2-3-4'], ['3-4-5']])
})
it('should compute uuid bundles to fetch based on transfer limit - content size not defined on an item', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 20,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
},
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
expect(result).toEqual([
[
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000002',
],
])
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 50)
expect(result).toEqual([['1-2-3', '2-3-4', '3-4-5']])
})
it('should compute uuid bundles to fetch based on transfer limit - first item over the limit', async () => {
const query = {} as jest.Mocked<ItemQuery>
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
{
uuid: '1-2-3',
contentSize: 50,
},
{
uuid: '2-3-4',
contentSize: 20,
},
{
uuid: '3-4-5',
contentSize: 20,
},
const itemContentSizeDescriptors = [
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 50).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
expect(result).toEqual([
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
['00000000-0000-0000-0000-000000000002'],
])
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 40)
expect(result).toEqual([['1-2-3', '2-3-4'], ['3-4-5']])
})
})
})
@@ -1,27 +1,28 @@
import { Logger } from 'winston'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ItemQuery } from './ItemQuery'
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
constructor(private logger: Logger) {}
async computeItemUuidsToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<string>> {
async computeItemUuidsToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
): Promise<Array<string>> {
const itemUuidsToFetch = []
const itemContentSizes = await this.itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
let totalContentSizeInBytes = 0
for (const itemContentSize of itemContentSizes) {
const contentSize = itemContentSize.contentSize ?? 0
for (const itemContentSize of itemContentSizeDescriptors) {
const contentSize = itemContentSize.props.contentSize ?? 0
itemUuidsToFetch.push(itemContentSize.uuid)
itemUuidsToFetch.push(itemContentSize.props.uuid.value)
totalContentSizeInBytes += contentSize
const transferLimitBreached = this.isTransferLimitBreached({
totalContentSizeInBytes,
bytesTransferLimit,
itemUuidsToFetch,
itemContentSizes,
itemContentSizeDescriptors,
})
if (transferLimitBreached) {
@@ -32,22 +33,24 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
return itemUuidsToFetch
}
async computeItemUuidBundlesToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<Array<string>>> {
async computeItemUuidBundlesToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
): Promise<Array<Array<string>>> {
let itemUuidsToFetch = []
const itemContentSizes = await this.itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
let totalContentSizeInBytes = 0
const bundles = []
for (const itemContentSize of itemContentSizes) {
const contentSize = itemContentSize.contentSize ?? 0
for (const itemContentSize of itemContentSizeDescriptors) {
const contentSize = itemContentSize.props.contentSize ?? 0
itemUuidsToFetch.push(itemContentSize.uuid)
itemUuidsToFetch.push(itemContentSize.props.uuid.value)
totalContentSizeInBytes += contentSize
const transferLimitBreached = this.isTransferLimitBreached({
totalContentSizeInBytes,
bytesTransferLimit,
itemUuidsToFetch,
itemContentSizes,
itemContentSizeDescriptors,
})
if (transferLimitBreached) {
@@ -68,11 +71,11 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
totalContentSizeInBytes: number
bytesTransferLimit: number
itemUuidsToFetch: Array<string>
itemContentSizes: Array<{ uuid: string; contentSize: number | null }>
itemContentSizeDescriptors: ItemContentSizeDescriptor[]
}): boolean {
const transferLimitBreached = dto.totalContentSizeInBytes >= dto.bytesTransferLimit
const transferLimitBreachedAtFirstItem =
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizes.length > 1
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizeDescriptors.length > 1
if (transferLimitBreachedAtFirstItem) {
this.logger.warn(
@@ -1,6 +1,12 @@
import { ItemQuery } from './ItemQuery'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
export interface ItemTransferCalculatorInterface {
computeItemUuidsToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<string>>
computeItemUuidBundlesToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<Array<string>>>
computeItemUuidsToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
): Promise<Array<string>>
computeItemUuidBundlesToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
): Promise<Array<Array<string>>>
}
@@ -40,10 +40,8 @@ describe('SharedVaultFilter', () => {
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
@@ -254,10 +252,8 @@ describe('SharedVaultFilter', () => {
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
@@ -427,10 +423,8 @@ describe('SharedVaultFilter', () => {
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
@@ -589,10 +583,8 @@ describe('SharedVaultFilter', () => {
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
@@ -26,10 +26,8 @@ describe('SharedVaultSnjsFilter', () => {
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
@@ -1,7 +1,3 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
export interface KeySystemAssociationProps {
itemUuid: Uuid
keySystemIdentifier: string
timestamps: Timestamps
}
@@ -1,12 +1,8 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { KeySystemAssociation } from './KeySystemAssociation'
describe('KeySystemAssociation', () => {
it('should create an entity', () => {
const entityOrError = KeySystemAssociation.create({
timestamps: Timestamps.create(123456789, 123456789).getValue(),
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
keySystemIdentifier: '00000000-0000-0000-0000-000000000000',
})
@@ -1,9 +0,0 @@
import { Uuid } from '@standardnotes/domain-core'
import { KeySystemAssociation } from './KeySystemAssociation'
export interface KeySystemAssociationRepositoryInterface {
save(keySystem: KeySystemAssociation): Promise<void>
remove(keySystem: KeySystemAssociation): Promise<void>
findByItemUuid(itemUuid: Uuid): Promise<KeySystemAssociation | null>
}
@@ -1,12 +1,10 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { Uuid } from '@standardnotes/domain-core'
import { SharedVaultAssociation } from './SharedVaultAssociation'
describe('SharedVaultAssociation', () => {
it('should create an entity', () => {
const entityOrError = SharedVaultAssociation.create({
timestamps: Timestamps.create(123456789, 123456789).getValue(),
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
})
@@ -1,8 +1,6 @@
import { Timestamps, Uuid } from '@standardnotes/domain-core'
import { Uuid } from '@standardnotes/domain-core'
export interface SharedVaultAssociationProps {
lastEditedBy: Uuid
sharedVaultUuid: Uuid
itemUuid: Uuid
timestamps: Timestamps
}
@@ -1,9 +0,0 @@
import { Uuid } from '@standardnotes/domain-core'
import { SharedVaultAssociation } from './SharedVaultAssociation'
export interface SharedVaultAssociationRepositoryInterface {
save(sharedVaultAssociation: SharedVaultAssociation): Promise<void>
remove(sharedVaultAssociation: SharedVaultAssociation): Promise<void>
findByItemUuid(itemUuid: Uuid): Promise<SharedVaultAssociation | null>
}
@@ -60,10 +60,8 @@ describe('DetermineSharedVaultOperationOnItem', () => {
existingItem = Item.create({
...existingItem.props,
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: existingItem.uuid,
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
@@ -88,10 +86,8 @@ describe('DetermineSharedVaultOperationOnItem', () => {
existingItem = Item.create({
...existingItem.props,
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: existingItem.uuid,
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
@@ -132,10 +128,8 @@ describe('DetermineSharedVaultOperationOnItem', () => {
existingItem = Item.create({
...existingItem.props,
sharedVaultAssociation: SharedVaultAssociation.create({
itemUuid: existingItem.uuid,
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue(),
}).getValue()
@@ -1,15 +1,17 @@
import 'reflect-metadata'
import { ContentType } from '@standardnotes/domain-core'
import { ContentType, RoleName } from '@standardnotes/domain-core'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { CheckIntegrity } from './CheckIntegrity'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
describe('CheckIntegrity', () => {
let itemRepository: ItemRepositoryInterface
let itemRepositoryResolver: ItemRepositoryResolverInterface
const createUseCase = () => new CheckIntegrity(itemRepository)
const createUseCase = () => new CheckIntegrity(itemRepositoryResolver)
beforeEach(() => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
@@ -40,6 +42,9 @@ describe('CheckIntegrity', () => {
content_type: ContentType.TYPES.File,
},
])
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
})
it('should return an empty result if there are no integrity mismatches', async () => {
@@ -63,6 +68,7 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 5,
},
],
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.getValue()).toEqual([])
})
@@ -88,6 +94,7 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 5,
},
],
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.getValue()).toEqual([
{
@@ -114,6 +121,7 @@ describe('CheckIntegrity', () => {
updated_at_timestamp: 5,
},
],
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.getValue()).toEqual([
{
@@ -122,4 +130,27 @@ describe('CheckIntegrity', () => {
},
])
})
it('should return error if the role names are invalid', async () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
integrityPayloads: [
{
uuid: '1-2-3',
updated_at_timestamp: 1,
},
{
uuid: '2-3-4',
updated_at_timestamp: 2,
},
{
uuid: '5-6-7',
updated_at_timestamp: 5,
},
],
roleNames: ['invalid-role-name'],
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Invalid role name: invalid-role-name')
})
})
@@ -1,15 +1,22 @@
import { IntegrityPayload } from '@standardnotes/responses'
import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core'
import { ContentType, Result, RoleNameCollection, UseCaseInterface } from '@standardnotes/domain-core'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { CheckIntegrityDTO } from './CheckIntegrityDTO'
import { ExtendedIntegrityPayload } from '../../../Item/ExtendedIntegrityPayload'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
export class CheckIntegrity implements UseCaseInterface<IntegrityPayload[]> {
constructor(private itemRepository: ItemRepositoryInterface) {}
constructor(private itemRepositoryResolver: ItemRepositoryResolverInterface) {}
async execute(dto: CheckIntegrityDTO): Promise<Result<IntegrityPayload[]>> {
const serverItemIntegrityPayloads = await this.itemRepository.findItemsForComputingIntegrityPayloads(dto.userUuid)
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
if (roleNamesOrError.isFailed()) {
return Result.fail(roleNamesOrError.getError())
}
const roleNames = roleNamesOrError.getValue()
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
const serverItemIntegrityPayloads = await itemRepository.findItemsForComputingIntegrityPayloads(dto.userUuid)
const serverItemIntegrityPayloadsMap = new Map<string, ExtendedIntegrityPayload>()
for (const serverItemIntegrityPayload of serverItemIntegrityPayloads) {
@@ -2,5 +2,6 @@ import { IntegrityPayload } from '@standardnotes/responses'
export type CheckIntegrityDTO = {
userUuid: string
roleNames: string[]
integrityPayloads: IntegrityPayload[]
}
@@ -3,26 +3,43 @@ import { Item } from '../../../Item/Item'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { GetItem } from './GetItem'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
import { RoleName } from '@standardnotes/domain-core'
describe('GetItem', () => {
let itemRepository: ItemRepositoryInterface
let itemRepositoryResolver: ItemRepositoryResolverInterface
const createUseCase = () => new GetItem(itemRepository)
const createUseCase = () => new GetItem(itemRepositoryResolver)
beforeEach(() => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
})
it('should fail if item is not found', async () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemUuid: '2-3-4',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not find item with uuid 2-3-4')
})
it('should fail if the role names are invalid', async () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemUuid: '2-3-4',
roleNames: ['invalid-role-name'],
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Invalid role name: invalid-role-name')
})
it('should succeed if item is found', async () => {
const item = {} as jest.Mocked<Item>
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
@@ -30,6 +47,7 @@ describe('GetItem', () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
itemUuid: '2-3-4',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.getValue()).toEqual(item)
@@ -1,14 +1,21 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { Result, RoleNameCollection, UseCaseInterface } from '@standardnotes/domain-core'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { GetItemDTO } from './GetItemDTO'
import { Item } from '../../../Item/Item'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
export class GetItem implements UseCaseInterface<Item> {
constructor(private itemRepository: ItemRepositoryInterface) {}
constructor(private itemRepositoryResolver: ItemRepositoryResolverInterface) {}
async execute(dto: GetItemDTO): Promise<Result<Item>> {
const item = await this.itemRepository.findByUuidAndUserUuid(dto.itemUuid, dto.userUuid)
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
if (roleNamesOrError.isFailed()) {
return Result.fail(roleNamesOrError.getError())
}
const roleNames = roleNamesOrError.getValue()
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
const item = await itemRepository.findByUuidAndUserUuid(dto.itemUuid, dto.userUuid)
if (item === null) {
return Result.fail(`Could not find item with uuid ${dto.itemUuid}`)
@@ -1,4 +1,5 @@
export type GetItemDTO = {
userUuid: string
itemUuid: string
roleNames: string[]
}
@@ -3,11 +3,14 @@ import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalculatorInterface'
import { GetItems } from './GetItems'
import { Item } from '../../../Item/Item'
import { ContentType, Dates, Timestamps, Uuid } from '@standardnotes/domain-core'
import { ContentType, Dates, RoleName, Timestamps, Uuid } from '@standardnotes/domain-core'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
import { ItemContentSizeDescriptor } from '../../../Item/ItemContentSizeDescriptor'
describe('GetItems', () => {
let itemRepository: ItemRepositoryInterface
let itemRepositoryResolver: ItemRepositoryResolverInterface
const contentSizeTransferLimit = 100
let itemTransferCalculator: ItemTransferCalculatorInterface
let timer: TimerInterface
@@ -16,7 +19,14 @@ describe('GetItems', () => {
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
const createUseCase = () =>
new GetItems(itemRepository, contentSizeTransferLimit, itemTransferCalculator, timer, maxItemsSyncLimit)
new GetItems(
itemRepositoryResolver,
sharedVaultUserRepository,
contentSizeTransferLimit,
itemTransferCalculator,
timer,
maxItemsSyncLimit,
)
beforeEach(() => {
item = Item.create({
@@ -36,6 +46,12 @@ describe('GetItems', () => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findAll = jest.fn().mockResolvedValue([item])
itemRepository.countAll = jest.fn().mockResolvedValue(1)
itemRepository.findContentSizeForComputingTransferLimit = jest
.fn()
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockResolvedValue(['item-uuid'])
@@ -53,6 +69,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
cursorToken: undefined,
contentType: undefined,
limit: 10,
@@ -73,6 +90,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
cursorToken: undefined,
contentType: undefined,
limit: undefined,
@@ -91,6 +109,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
cursorToken: 'MjowLjAwMDEyMw==',
contentType: undefined,
limit: undefined,
@@ -112,6 +131,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
syncToken,
contentType: undefined,
limit: undefined,
@@ -133,6 +153,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
syncToken,
contentType: undefined,
limit: undefined,
@@ -147,6 +168,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
cursorToken: undefined,
contentType: undefined,
limit: 200,
@@ -165,6 +187,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: 'invalid',
roleNames: [RoleName.NAMES.CoreUser],
cursorToken: undefined,
contentType: undefined,
limit: undefined,
@@ -174,6 +197,21 @@ describe('GetItems', () => {
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
it('should return error for invalid role names', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: ['invalid'],
cursorToken: undefined,
contentType: undefined,
limit: undefined,
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Invalid role name: invalid')
})
it('should filter shared vault uuids user wants to sync with the ones it has access to', async () => {
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([
{
@@ -187,6 +225,7 @@ describe('GetItems', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
cursorToken: undefined,
contentType: undefined,
limit: undefined,
@@ -1,19 +1,21 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Time, TimerInterface } from '@standardnotes/time'
import { Item } from '../../../Item/Item'
import { GetItemsResult } from './GetItemsResult'
import { ItemQuery } from '../../../Item/ItemQuery'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalculatorInterface'
import { GetItemsDTO } from './GetItemsDTO'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
export class GetItems implements UseCaseInterface<GetItemsResult> {
private readonly DEFAULT_ITEMS_LIMIT = 150
private readonly SYNC_TOKEN_VERSION = 2
constructor(
private itemRepository: ItemRepositoryInterface,
private itemRepositoryResolver: ItemRepositoryResolverInterface,
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private contentSizeTransferLimit: number,
private itemTransferCalculator: ItemTransferCalculatorInterface,
private timer: TimerInterface,
@@ -33,10 +35,23 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
}
const userUuid = userUuidOrError.getValue()
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
if (roleNamesOrError.isFailed()) {
return Result.fail(roleNamesOrError.getError())
}
const roleNames = roleNamesOrError.getValue()
const syncTimeComparison = dto.cursorToken ? '>=' : '>'
const limit = dto.limit === undefined || dto.limit < 1 ? this.DEFAULT_ITEMS_LIMIT : dto.limit
const upperBoundLimit = limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit
const sharedVaultUsers = await this.sharedVaultUserRepository.findByUserUuid(userUuid)
const userSharedVaultUuids = sharedVaultUsers.map((sharedVaultUser) => sharedVaultUser.props.sharedVaultUuid.value)
const exclusiveSharedVaultUuids = dto.sharedVaultUuids
? dto.sharedVaultUuids.filter((sharedVaultUuid) => userSharedVaultUuids.includes(sharedVaultUuid))
: undefined
const itemQuery: ItemQuery = {
userUuid: userUuid.value,
lastSyncTime: lastSyncTime ?? undefined,
@@ -46,23 +61,26 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
limit: upperBoundLimit,
includeSharedVaultUuids: undefined,
exclusiveSharedVaultUuids: undefined,
includeSharedVaultUuids: !dto.sharedVaultUuids ? userSharedVaultUuids : undefined,
exclusiveSharedVaultUuids,
}
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
const itemContentSizeDescriptors = await itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
itemQuery,
itemContentSizeDescriptors,
this.contentSizeTransferLimit,
)
let items: Array<Item> = []
if (itemUuidsToFetch.length > 0) {
items = await this.itemRepository.findAll({
items = await itemRepository.findAll({
uuids: itemUuidsToFetch,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
})
}
const totalItemsCount = await this.itemRepository.countAll(itemQuery)
const totalItemsCount = await itemRepository.countAll(itemQuery)
let cursorToken = undefined
if (totalItemsCount > upperBoundLimit) {
@@ -1,5 +1,6 @@
export interface GetItemsDTO {
userUuid: string
roleNames: string[]
syncToken?: string | null
cursorToken?: string | null
limit?: number
@@ -5,13 +5,15 @@ import { SaveItems } from './SaveItems'
import { SaveNewItem } from '../SaveNewItem/SaveNewItem'
import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
import { Logger } from 'winston'
import { ContentType, Dates, Result, Timestamps, Uuid } from '@standardnotes/domain-core'
import { ContentType, Dates, Result, RoleName, Timestamps, Uuid } from '@standardnotes/domain-core'
import { ItemHash } from '../../../Item/ItemHash'
import { Item } from '../../../Item/Item'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
describe('SaveItems', () => {
let itemSaveValidator: ItemSaveValidatorInterface
let itemRepository: ItemRepositoryInterface
let itemRepositoryResolver: ItemRepositoryResolverInterface
let timer: TimerInterface
let saveNewItem: SaveNewItem
let updateExistingItem: UpdateExistingItem
@@ -20,7 +22,7 @@ describe('SaveItems', () => {
let savedItem: Item
const createUseCase = () =>
new SaveItems(itemSaveValidator, itemRepository, timer, saveNewItem, updateExistingItem, logger)
new SaveItems(itemSaveValidator, itemRepositoryResolver, timer, saveNewItem, updateExistingItem, logger)
beforeEach(() => {
itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
@@ -29,6 +31,9 @@ describe('SaveItems', () => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findByUuid = jest.fn().mockResolvedValue(null)
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
@@ -83,6 +88,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -91,6 +97,7 @@ describe('SaveItems', () => {
itemHash: itemHash1,
userUuid: 'user-uuid',
sessionUuid: 'session-uuid',
roleNames: ['CORE_USER'],
})
})
@@ -106,6 +113,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -129,6 +137,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -150,6 +159,7 @@ describe('SaveItems', () => {
readOnlyAccess: true,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -172,6 +182,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -190,6 +201,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -208,6 +220,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -216,6 +229,7 @@ describe('SaveItems', () => {
existingItem: savedItem,
sessionUuid: 'session-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
roleNames: ['CORE_USER'],
})
})
@@ -232,6 +246,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -256,6 +271,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
@@ -301,9 +317,27 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().syncToken).toEqual('MjowLjAwMDE2')
})
it('should fail if the role names are invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
itemHashes: [itemHash1],
userUuid: 'user-uuid',
apiVersion: '2',
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
roleNames: ['invalid-role-name'],
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Invalid role name: invalid-role-name')
})
})
@@ -1,4 +1,4 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SaveItemsResult } from './SaveItemsResult'
import { SaveItemsDTO } from './SaveItemsDTO'
@@ -7,17 +7,17 @@ import { ItemConflict } from '../../../Item/ItemConflict'
import { ConflictType } from '@standardnotes/responses'
import { Time, TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { ItemSaveValidatorInterface } from '../../../Item/SaveValidator/ItemSaveValidatorInterface'
import { SaveNewItem } from '../SaveNewItem/SaveNewItem'
import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
export class SaveItems implements UseCaseInterface<SaveItemsResult> {
private readonly SYNC_TOKEN_VERSION = 2
constructor(
private itemSaveValidator: ItemSaveValidatorInterface,
private itemRepository: ItemRepositoryInterface,
private itemRepositoryResolver: ItemRepositoryResolverInterface,
private timer: TimerInterface,
private saveNewItem: SaveNewItem,
private updateExistingItem: UpdateExistingItem,
@@ -28,6 +28,12 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
const savedItems: Array<Item> = []
const conflicts: Array<ItemConflict> = []
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
if (roleNamesOrError.isFailed()) {
return Result.fail(roleNamesOrError.getError())
}
const roleNames = roleNamesOrError.getValue()
const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds()
for (const itemHash of dto.itemHashes) {
@@ -42,7 +48,8 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
}
const itemUuid = itemUuidOrError.getValue()
const existingItem = await this.itemRepository.findByUuid(itemUuid, false)
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
const existingItem = await itemRepository.findByUuid(itemUuid)
if (dto.readOnlyAccess) {
conflicts.push({
@@ -78,6 +85,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
itemHash,
sessionUuid: dto.sessionUuid,
performingUserUuid: dto.userUuid,
roleNames: dto.roleNames,
})
if (udpatedItemOrError.isFailed()) {
this.logger.error(
@@ -100,6 +108,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
userUuid: dto.userUuid,
itemHash,
sessionUuid: dto.sessionUuid,
roleNames: dto.roleNames,
})
if (newItemOrError.isFailed()) {
this.logger.error(
@@ -7,4 +7,5 @@ export interface SaveItemsDTO {
readOnlyAccess: boolean
sessionUuid: string | null
snjsVersion: string
roleNames: string[]
}
@@ -4,20 +4,22 @@ import { SaveNewItem } from './SaveNewItem'
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { ItemHash } from '../../../Item/ItemHash'
import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { ContentType, Dates, Result, RoleName, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { Item } from '../../../Item/Item'
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
describe('SaveNewItem', () => {
let itemRepository: ItemRepositoryInterface
let itemRepositoryResolver: ItemRepositoryResolverInterface
let timer: TimerInterface
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let itemHash1: ItemHash
let item1: Item
const createUseCase = () => new SaveNewItem(itemRepository, timer, domainEventPublisher, domainEventFactory)
const createUseCase = () => new SaveNewItem(itemRepositoryResolver, timer, domainEventPublisher, domainEventFactory)
beforeEach(() => {
const timeHelper = new Timer()
@@ -62,6 +64,9 @@ describe('SaveNewItem', () => {
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.save = jest.fn()
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
@@ -85,6 +90,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -106,6 +112,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -125,6 +132,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -142,6 +150,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -161,6 +170,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -180,6 +190,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -196,6 +207,20 @@ describe('SaveNewItem', () => {
userUuid: '00000000-0000-0000-0000-00000000000',
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeTruthy()
})
it('returns a failure if the role names are invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
roleNames: ['invalid'],
})
expect(result.isFailed()).toBeTruthy()
@@ -206,6 +231,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-00000000000',
itemHash: itemHash1,
})
@@ -223,6 +249,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -240,6 +267,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -257,6 +285,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -276,6 +305,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -297,6 +327,7 @@ describe('SaveNewItem', () => {
userUuid: '00000000-0000-0000-0000-00000000000',
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
roleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBeTruthy()
@@ -313,6 +344,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -339,6 +371,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -360,14 +393,12 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue().props.keySystemAssociation?.props.itemUuid.value).toEqual(
'00000000-0000-0000-0000-000000000000',
)
expect(itemRepository.save).toHaveBeenCalled()
})
@@ -381,6 +412,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})
@@ -403,6 +435,7 @@ describe('SaveNewItem', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
roleNames: [RoleName.NAMES.CoreUser],
sessionUuid: '00000000-0000-0000-0000-000000000001',
itemHash: itemHash1,
})

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