mirror of
https://github.com/standardnotes/server
synced 2026-01-19 02:06:04 -05:00
Compare commits
17 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fae4553fc8 | ||
|
|
cb74b23e45 | ||
|
|
af8f12c33a | ||
|
|
a148c4d1f6 | ||
|
|
f7190c0c9c | ||
|
|
c00d7765a9 | ||
|
|
2b651d86e2 | ||
|
|
9be3517093 | ||
|
|
fcfedaf7e7 | ||
|
|
0b82794e9c | ||
|
|
2a52e398cb | ||
|
|
c31e882ad2 | ||
|
|
2f0903e0eb | ||
|
|
2396053bc1 | ||
|
|
17fd12305e | ||
|
|
425ea4374d | ||
|
|
c076c3c74a |
4
.github/workflows/common-e2e.yml
vendored
4
.github/workflows/common-e2e.yml
vendored
@@ -115,6 +115,8 @@ jobs:
|
||||
echo "DB_TYPE=${{ matrix.db_type }}" >> packages/home-server/.env
|
||||
echo "REDIS_URL=redis://cache" >> packages/home-server/.env
|
||||
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
|
||||
echo "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
|
||||
echo "E2E_TESTING=true" >> packages/home-server/.env
|
||||
|
||||
- name: Run Server
|
||||
run: nohup yarn workspace @standardnotes/home-server start &
|
||||
@@ -125,4 +127,4 @@ jobs:
|
||||
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html?skip_paid_features=true
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
|
||||
|
||||
112
.pnp.cjs
generated
112
.pnp.cjs
generated
@@ -4560,16 +4560,16 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/api", [\
|
||||
["npm:1.26.25", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip/node_modules/@standardnotes/api/",\
|
||||
["npm:1.26.26", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.26.26-4338a5fe92-db41aedfa3.zip/node_modules/@standardnotes/api/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/api", "npm:1.26.25"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/models", "npm:1.46.7"],\
|
||||
["@standardnotes/responses", "npm:1.13.26"],\
|
||||
["@standardnotes/models", "npm:1.46.8"],\
|
||||
["@standardnotes/responses", "npm:1.13.27"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/utils", "npm:1.17.4"],\
|
||||
["@standardnotes/utils", "npm:1.17.5"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -4634,17 +4634,17 @@ const RAW_RUNTIME_STATE =
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
|
||||
["@simplewebauthn/server", "npm:7.2.0"],\
|
||||
["@simplewebauthn/typescript-types", "npm:7.0.0"],\
|
||||
["@standardnotes/api", "npm:1.26.25"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/features", "npm:1.59.5"],\
|
||||
["@standardnotes/features", "npm:1.59.7"],\
|
||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
||||
["@standardnotes/responses", "npm:1.13.24"],\
|
||||
["@standardnotes/responses", "npm:1.13.27"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/settings", "workspace:packages/settings"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.4"],\
|
||||
["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/bcryptjs", "npm:2.4.2"],\
|
||||
@@ -4815,21 +4815,10 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/features", [\
|
||||
["npm:1.59.5", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.59.5-83c83acde9-173b1f5d52.zip/node_modules/@standardnotes/features/",\
|
||||
["npm:1.59.7", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.59.7-27c3e5296e-1632d64cc1.zip/node_modules/@standardnotes/features/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/features", "npm:1.59.5"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.59.6", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip/node_modules/@standardnotes/features/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/features", "npm:1.59.6"],\
|
||||
["@standardnotes/features", "npm:1.59.7"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
@@ -4850,7 +4839,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.4"],\
|
||||
["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/connect-busboy", "npm:1.0.0"],\
|
||||
@@ -4930,15 +4919,16 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/models", [\
|
||||
["npm:1.46.7", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip/node_modules/@standardnotes/models/",\
|
||||
["npm:1.46.8", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.46.8-bc0390832e-8404340f27.zip/node_modules/@standardnotes/models/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/models", "npm:1.46.7"],\
|
||||
["@standardnotes/models", "npm:1.46.8"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.59.6"],\
|
||||
["@standardnotes/responses", "npm:1.13.26"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/features", "npm:1.59.7"],\
|
||||
["@standardnotes/responses", "npm:1.13.27"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.4"],\
|
||||
["@standardnotes/utils", "npm:1.17.4"],\
|
||||
["@standardnotes/utils", "npm:1.17.5"],\
|
||||
["lodash", "npm:4.17.21"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -4963,23 +4953,12 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/responses", [\
|
||||
["npm:1.13.24", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.24-3b4167c7ea-3bcfee90f0.zip/node_modules/@standardnotes/responses/",\
|
||||
["npm:1.13.27", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.27-829dec3e6e-9bf55e5f02.zip/node_modules/@standardnotes/responses/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/responses", "npm:1.13.24"],\
|
||||
["@standardnotes/responses", "npm:1.13.27"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.59.5"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.13.26", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip/node_modules/@standardnotes/responses/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/responses", "npm:1.13.26"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.59.6"],\
|
||||
["@standardnotes/features", "npm:1.59.7"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
@@ -4994,12 +4973,12 @@ const RAW_RUNTIME_STATE =
|
||||
["@aws-sdk/client-s3", "npm:3.342.0"],\
|
||||
["@aws-sdk/client-sqs", "npm:3.342.0"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
|
||||
["@standardnotes/api", "npm:1.26.25"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/responses", "npm:1.13.24"],\
|
||||
["@standardnotes/responses", "npm:1.13.27"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/cors", "npm:2.8.13"],\
|
||||
@@ -5136,14 +5115,6 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/sncrypto-common", [\
|
||||
["npm:1.13.3", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.3-97ef3850ce-a73af90962.zip/node_modules/@standardnotes/sncrypto-common/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.13.4", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip/node_modules/@standardnotes/sncrypto-common/",\
|
||||
"packageDependencies": [\
|
||||
@@ -5158,7 +5129,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageLocation": "./packages/sncrypto-node/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.4"],\
|
||||
["@types/jest", "npm:29.5.2"],\
|
||||
["@types/node", "npm:20.2.5"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.59.8"],\
|
||||
@@ -5186,12 +5157,12 @@ const RAW_RUNTIME_STATE =
|
||||
["@aws-sdk/client-sns", "npm:3.342.0"],\
|
||||
["@aws-sdk/client-sqs", "npm:3.342.0"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
|
||||
["@standardnotes/api", "npm:1.26.25"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/responses", "npm:1.13.24"],\
|
||||
["@standardnotes/responses", "npm:1.13.27"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/settings", "workspace:packages/settings"],\
|
||||
["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\
|
||||
@@ -5262,21 +5233,10 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/utils", [\
|
||||
["npm:1.16.5", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.16.5-47f537f49f-d5caa7181f.zip/node_modules/@standardnotes/utils/",\
|
||||
["npm:1.17.5", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.17.5-210b60222d-47e8520174.zip/node_modules/@standardnotes/utils/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/utils", "npm:1.16.5"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["dompurify", "npm:2.4.5"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.17.4", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip/node_modules/@standardnotes/utils/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/utils", "npm:1.17.4"],\
|
||||
["@standardnotes/utils", "npm:1.17.5"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["dompurify", "npm:2.4.5"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
@@ -5292,14 +5252,14 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
|
||||
["@aws-sdk/client-sqs", "npm:3.342.0"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
|
||||
["@standardnotes/api", "npm:1.26.25"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/responses", "npm:1.13.24"],\
|
||||
["@standardnotes/responses", "npm:1.13.27"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/utils", "npm:1.16.5"],\
|
||||
["@standardnotes/utils", "npm:1.17.5"],\
|
||||
["@types/cors", "npm:2.8.13"],\
|
||||
["@types/express", "npm:4.17.17"],\
|
||||
["@types/ioredis", "npm:5.0.0"],\
|
||||
|
||||
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.
Binary file not shown.
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.25.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.9...@standardnotes/analytics@2.25.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [2.24.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.8...@standardnotes/analytics@2.24.9) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.24.9",
|
||||
"version": "2.25.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Result, Entity, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { StatisticMeasureProps } from './StatisticMeasureProps'
|
||||
|
||||
export class StatisticMeasure extends Entity<StatisticMeasureProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.props.name.value
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { SubscriptionProps } from './SubscriptionProps'
|
||||
|
||||
export class Subscription extends Entity<SubscriptionProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: SubscriptionProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { UserProps } from './UserProps'
|
||||
|
||||
export class User extends Entity<UserProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: UserProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -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.65.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.65.6...@standardnotes/api-gateway@1.65.7) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.65.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.65.5...@standardnotes/api-gateway@1.65.6) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.65.6",
|
||||
"version": "1.65.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.125.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.124.2...@standardnotes/auth-server@1.125.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [1.124.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.124.1...@standardnotes/auth-server@1.124.2) (2023-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** allow custom atributtes for activating premium features ([f7190c0](https://github.com/standardnotes/server/commit/f7190c0c9c2d105f97d1cf980ce6a4f0dae34805))
|
||||
|
||||
## [1.124.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.124.0...@standardnotes/auth-server@1.124.1) (2023-07-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **files:** handling unlimited storage quota on home server ([9be3517](https://github.com/standardnotes/server/commit/9be3517093f8dd7bbdd7507c1e2ff059e6c9a889))
|
||||
|
||||
# [1.124.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.123.2...@standardnotes/auth-server@1.124.0) (2023-07-13)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add overriding subscription settings on home server ([#656](https://github.com/standardnotes/server/issues/656)) ([0b82794](https://github.com/standardnotes/server/commit/0b82794e9c7ed82cfc08a92eafc016fbde5c4fcc))
|
||||
|
||||
## [1.123.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.123.1...@standardnotes/auth-server@1.123.2) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.123.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.123.0...@standardnotes/auth-server@1.123.1) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.123.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.122.2...@standardnotes/auth-server@1.123.0) (2023-07-12)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.123.0",
|
||||
"version": "1.125.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -42,17 +42,17 @@
|
||||
"@cbor-extract/cbor-extract-linux-x64": "^2.1.1",
|
||||
"@simplewebauthn/server": "^7.2.0",
|
||||
"@simplewebauthn/typescript-types": "^7.0.0",
|
||||
"@standardnotes/api": "^1.26.25",
|
||||
"@standardnotes/api": "^1.26.26",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/features": "^1.58.12",
|
||||
"@standardnotes/features": "^1.59.7",
|
||||
"@standardnotes/predicates": "workspace:*",
|
||||
"@standardnotes/responses": "^1.13.9",
|
||||
"@standardnotes/responses": "^1.13.27",
|
||||
"@standardnotes/security": "workspace:*",
|
||||
"@standardnotes/settings": "workspace:*",
|
||||
"@standardnotes/sncrypto-common": "^1.9.0",
|
||||
"@standardnotes/sncrypto-common": "^1.13.4",
|
||||
"@standardnotes/sncrypto-node": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"axios": "^1.1.3",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Result, ServiceInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export interface AuthServiceInterface extends ServiceInterface {
|
||||
activatePremiumFeatures(username: string): Promise<Result<string>>
|
||||
activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>>
|
||||
}
|
||||
|
||||
@@ -793,6 +793,7 @@ export class ContainerConfigLoader {
|
||||
new ActivatePremiumFeatures(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get(TYPES.Auth_SubscriptionSettingService),
|
||||
container.get(TYPES.Auth_RoleService),
|
||||
container.get(TYPES.Auth_Timer),
|
||||
),
|
||||
|
||||
@@ -24,14 +24,18 @@ export class Service implements AuthServiceInterface {
|
||||
this.serviceContainer.register(this.getId(), this)
|
||||
}
|
||||
|
||||
async activatePremiumFeatures(username: string): Promise<Result<string>> {
|
||||
async activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>> {
|
||||
if (!this.container) {
|
||||
return Result.fail('Container not initialized')
|
||||
}
|
||||
|
||||
const activatePremiumFeatures = this.container.get(TYPES.Auth_ActivatePremiumFeatures) as ActivatePremiumFeatures
|
||||
|
||||
return activatePremiumFeatures.execute({ username })
|
||||
return activatePremiumFeatures.execute(dto)
|
||||
}
|
||||
|
||||
async handleRequest(request: never, response: never, endpointOrMethodIdentifier: string): Promise<unknown> {
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { AuthenticatorProps } from './AuthenticatorProps'
|
||||
|
||||
export class Authenticator extends Entity<AuthenticatorProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: AuthenticatorProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { AuthenticatorChallengeProps } from './AuthenticatorChallengeProps'
|
||||
|
||||
export class AuthenticatorChallenge extends Entity<AuthenticatorChallengeProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: AuthenticatorChallengeProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { EmergencyAccessInvitationProps } from './EmergencyAccessInvitationProps'
|
||||
|
||||
export class EmergencyAccessInvitation extends Entity<EmergencyAccessInvitationProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: EmergencyAccessInvitationProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscripti
|
||||
describe('FileRemovedEventHandler', () => {
|
||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let regularUser: User
|
||||
let sharedUser: User
|
||||
let event: FileRemovedEvent
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let regularSubscription: UserSubscription
|
||||
@@ -22,20 +23,24 @@ describe('FileRemovedEventHandler', () => {
|
||||
const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
regularUser = {
|
||||
uuid: '123',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
sharedUser = {
|
||||
uuid: '234',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(user),
|
||||
user: Promise.resolve(regularUser),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(user),
|
||||
user: Promise.resolve(sharedUser),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
@@ -93,10 +98,11 @@ describe('FileRemovedEventHandler', () => {
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: regularUser,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
user: Promise.resolve(user),
|
||||
user: Promise.resolve(regularUser),
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -118,10 +124,11 @@ describe('FileRemovedEventHandler', () => {
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: regularUser,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
user: Promise.resolve(user),
|
||||
user: Promise.resolve(regularUser),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -132,10 +139,11 @@ describe('FileRemovedEventHandler', () => {
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: sharedUser,
|
||||
userSubscription: {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: 'shared',
|
||||
user: Promise.resolve(user),
|
||||
user: Promise.resolve(sharedUser),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,6 +51,7 @@ export class FileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesUsed - byteSize).toString(),
|
||||
|
||||
@@ -76,6 +76,7 @@ describe('FileUploadedEventHandler', () => {
|
||||
unencryptedValue: '123',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
@@ -118,6 +119,7 @@ describe('FileUploadedEventHandler', () => {
|
||||
unencryptedValue: '468',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
@@ -143,6 +145,7 @@ describe('FileUploadedEventHandler', () => {
|
||||
unencryptedValue: '468',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
@@ -157,6 +160,7 @@ describe('FileUploadedEventHandler', () => {
|
||||
unencryptedValue: '468',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user,
|
||||
userSubscription: {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: 'shared',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSett
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
|
||||
@injectable()
|
||||
export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -36,14 +37,18 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, event.payload.fileByteSize)
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, user, event.payload.fileByteSize)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, event.payload.fileByteSize)
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, user, event.payload.fileByteSize)
|
||||
}
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(subscription: UserSubscription, byteSize: number): Promise<void> {
|
||||
private async updateUploadBytesUsedSetting(
|
||||
subscription: UserSubscription,
|
||||
user: User,
|
||||
byteSize: number,
|
||||
): Promise<void> {
|
||||
let bytesUsed = '0'
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: (await subscription.user).uuid,
|
||||
@@ -56,6 +61,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesUsed + byteSize).toString(),
|
||||
|
||||
@@ -114,8 +114,6 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
subscription,
|
||||
SubscriptionName.ProPlan,
|
||||
'123',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -66,11 +66,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
|
||||
await this.addUserRole(user, event.payload.subscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
event.payload.subscriptionName,
|
||||
user.uuid,
|
||||
)
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
|
||||
@@ -94,8 +94,6 @@ describe('SubscriptionReassignedEventHandler', () => {
|
||||
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
subscription,
|
||||
SubscriptionName.ProPlan,
|
||||
'123',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -63,11 +63,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
|
||||
},
|
||||
})
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
event.payload.subscriptionName,
|
||||
user.uuid,
|
||||
)
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: string): Promise<void> {
|
||||
|
||||
@@ -129,8 +129,6 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
|
||||
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
subscription,
|
||||
SubscriptionName.ProPlan,
|
||||
'123',
|
||||
)
|
||||
|
||||
expect(settingService.createOrReplace).toHaveBeenCalledWith({
|
||||
|
||||
@@ -95,11 +95,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
|
||||
await this.roleService.addUserRole(user, event.payload.subscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
event.payload.subscriptionName,
|
||||
user.uuid,
|
||||
)
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
await this.settingService.createOrReplace({
|
||||
user,
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { SessionTraceProps } from './SessionTraceProps'
|
||||
|
||||
export class SessionTrace extends Entity<SessionTraceProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: SessionTraceProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { User } from '../User/User'
|
||||
import { SubscriptionSettingProps } from './SubscriptionSettingProps'
|
||||
|
||||
export type CreateOrReplaceSubscriptionSettingDTO = {
|
||||
userSubscription: UserSubscription
|
||||
user: User
|
||||
props: SubscriptionSettingProps
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('SubscriptionSettingService', () => {
|
||||
userSubscription = {
|
||||
uuid: '1-2-3',
|
||||
user: Promise.resolve(user),
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
setting = {
|
||||
@@ -97,15 +98,70 @@ describe('SubscriptionSettingService', () => {
|
||||
})
|
||||
|
||||
it('should create default settings for a subscription', async () => {
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
SubscriptionName.PlusPlan,
|
||||
'1-2-3',
|
||||
)
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
|
||||
it('should create default settings for a subscription with overrides', async () => {
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesUsed,
|
||||
{
|
||||
value: '0',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
SettingName.NAMES.FileUploadBytesLimit,
|
||||
{
|
||||
value: '345',
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
replaceable: true,
|
||||
},
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, '123']]),
|
||||
)
|
||||
|
||||
expect(factory.createSubscriptionSetting).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
unencryptedValue: '0',
|
||||
},
|
||||
{
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
user: Promise.resolve(user),
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
)
|
||||
expect(factory.createSubscriptionSetting).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
sensitive: 0,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
unencryptedValue: '123',
|
||||
},
|
||||
{
|
||||
planName: SubscriptionName.PlusPlan,
|
||||
user: Promise.resolve(user),
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error if subscription setting is invalid', async () => {
|
||||
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
@@ -121,13 +177,7 @@ describe('SubscriptionSettingService', () => {
|
||||
]),
|
||||
)
|
||||
|
||||
await expect(
|
||||
createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
SubscriptionName.PlusPlan,
|
||||
'1-2-3',
|
||||
),
|
||||
).rejects.toThrow()
|
||||
await expect(createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should throw error if setting name is not a subscription setting when applying defaults', async () => {
|
||||
@@ -145,13 +195,7 @@ describe('SubscriptionSettingService', () => {
|
||||
]),
|
||||
)
|
||||
|
||||
await expect(
|
||||
createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
SubscriptionName.PlusPlan,
|
||||
'1-2-3',
|
||||
),
|
||||
).rejects.toThrow()
|
||||
await expect(createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should reassign existing default settings for a subscription if it is not replaceable', async () => {
|
||||
@@ -170,11 +214,7 @@ describe('SubscriptionSettingService', () => {
|
||||
)
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
SubscriptionName.PlusPlan,
|
||||
'1-2-3',
|
||||
)
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
@@ -195,11 +235,7 @@ describe('SubscriptionSettingService', () => {
|
||||
)
|
||||
subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
SubscriptionName.PlusPlan,
|
||||
'1-2-3',
|
||||
)
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
@@ -225,11 +261,7 @@ describe('SubscriptionSettingService', () => {
|
||||
} as jest.Mocked<UserSubscription>,
|
||||
])
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
SubscriptionName.PlusPlan,
|
||||
'1-2-3',
|
||||
)
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
|
||||
})
|
||||
@@ -239,11 +271,7 @@ describe('SubscriptionSettingService', () => {
|
||||
.fn()
|
||||
.mockReturnValue(undefined)
|
||||
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription,
|
||||
SubscriptionName.PlusPlan,
|
||||
'1-2-3',
|
||||
)
|
||||
await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription)
|
||||
|
||||
expect(subscriptionSettingRepository.save).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -251,6 +279,7 @@ describe('SubscriptionSettingService', () => {
|
||||
it("should create setting if it doesn't exist", async () => {
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
unencryptedValue: 'value',
|
||||
@@ -266,6 +295,7 @@ describe('SubscriptionSettingService', () => {
|
||||
await expect(
|
||||
createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
name: 'invalid',
|
||||
unencryptedValue: 'value',
|
||||
@@ -280,6 +310,7 @@ describe('SubscriptionSettingService', () => {
|
||||
await expect(
|
||||
createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.DropboxBackupFrequency,
|
||||
unencryptedValue: 'value',
|
||||
@@ -295,6 +326,7 @@ describe('SubscriptionSettingService', () => {
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
uuid: '1-2-3',
|
||||
name: SettingName.NAMES.FileUploadBytesLimit,
|
||||
@@ -312,6 +344,7 @@ describe('SubscriptionSettingService', () => {
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
...setting,
|
||||
unencryptedValue: 'value',
|
||||
@@ -327,6 +360,7 @@ describe('SubscriptionSettingService', () => {
|
||||
|
||||
const result = await createService().createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
...setting,
|
||||
uuid: '1-2-3',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@@ -36,17 +35,20 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
|
||||
|
||||
async applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription: UserSubscription,
|
||||
subscriptionName: SubscriptionName,
|
||||
userUuid: string,
|
||||
overrides?: Map<string, string>,
|
||||
): Promise<void> {
|
||||
const defaultSettingsWithValues =
|
||||
await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName(subscriptionName)
|
||||
await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName(
|
||||
userSubscription.planName,
|
||||
)
|
||||
if (defaultSettingsWithValues === undefined) {
|
||||
this.logger.warn(`Could not find settings for subscription: ${subscriptionName}`)
|
||||
this.logger.warn(`Could not find settings for subscription: ${userSubscription.planName}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const user = await userSubscription.user
|
||||
|
||||
for (const settingNameString of defaultSettingsWithValues.keys()) {
|
||||
const settingNameOrError = SettingName.create(settingNameString)
|
||||
if (settingNameOrError.isFailed()) {
|
||||
@@ -59,7 +61,11 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
|
||||
|
||||
const setting = defaultSettingsWithValues.get(settingName.value) as SettingDescription
|
||||
if (!setting.replaceable) {
|
||||
const existingSetting = await this.findPreviousSubscriptionSetting(settingName, userSubscription.uuid, userUuid)
|
||||
const existingSetting = await this.findPreviousSubscriptionSetting(
|
||||
settingName,
|
||||
userSubscription.uuid,
|
||||
user.uuid,
|
||||
)
|
||||
if (existingSetting !== null) {
|
||||
existingSetting.userSubscription = Promise.resolve(userSubscription)
|
||||
await this.subscriptionSettingRepository.save(existingSetting)
|
||||
@@ -68,11 +74,17 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
|
||||
}
|
||||
}
|
||||
|
||||
let unencryptedValue = setting.value
|
||||
if (overrides && overrides.has(settingName.value)) {
|
||||
unencryptedValue = overrides.get(settingName.value) as string
|
||||
}
|
||||
|
||||
await this.createOrReplace({
|
||||
userSubscription,
|
||||
user,
|
||||
props: {
|
||||
name: settingName.value,
|
||||
unencryptedValue: setting.value,
|
||||
unencryptedValue,
|
||||
serverEncryptionVersion: setting.serverEncryptionVersion,
|
||||
sensitive: setting.sensitive,
|
||||
},
|
||||
@@ -109,7 +121,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
|
||||
async createOrReplace(
|
||||
dto: CreateOrReplaceSubscriptionSettingDTO,
|
||||
): Promise<CreateOrReplaceSubscriptionSettingResponse> {
|
||||
const { userSubscription, props } = dto
|
||||
const { userSubscription, user, props } = dto
|
||||
|
||||
const settingNameOrError = SettingName.create(props.name)
|
||||
if (settingNameOrError.isFailed()) {
|
||||
@@ -121,7 +133,6 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
|
||||
throw new Error(`Setting ${settingName.value} is not a subscription setting`)
|
||||
}
|
||||
|
||||
const user = await userSubscription.user
|
||||
const existing = await this.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: user.uuid,
|
||||
userSubscriptionUuid: userSubscription.uuid,
|
||||
|
||||
@@ -8,8 +8,7 @@ import { SubscriptionSetting } from './SubscriptionSetting'
|
||||
export interface SubscriptionSettingServiceInterface {
|
||||
applyDefaultSubscriptionSettingsForSubscription(
|
||||
userSubscription: UserSubscription,
|
||||
subscriptionName: string,
|
||||
userUuid: string,
|
||||
overrides?: Map<string, string>,
|
||||
): Promise<void>
|
||||
createOrReplace(dto: CreateOrReplaceSubscriptionSettingDTO): Promise<CreateOrReplaceSubscriptionSettingResponse>
|
||||
findSubscriptionSettingWithDecryptedValue(dto: FindSubscriptionSettingDTO): Promise<SubscriptionSetting | null>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SettingDescription } from './SettingDescription'
|
||||
|
||||
export interface SubscriptionSettingsAssociationServiceInterface {
|
||||
getDefaultSettingsAndValuesForSubscriptionName(
|
||||
subscriptionName: SubscriptionName,
|
||||
subscriptionName: string,
|
||||
): Promise<Map<string, SettingDescription> | undefined>
|
||||
getFileUploadLimit(subscriptionName: SubscriptionName): Promise<number>
|
||||
}
|
||||
|
||||
@@ -106,8 +106,6 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
'PLUS_PLAN',
|
||||
'123',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -148,8 +146,6 @@ describe('AcceptSharedSubscriptionInvitation', () => {
|
||||
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
|
||||
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
|
||||
inviteeSubscription,
|
||||
'PLUS_PLAN',
|
||||
'123',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -92,11 +92,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
|
||||
|
||||
await this.addUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
inviteeSubscription,
|
||||
inviteeSubscription.planName as SubscriptionName,
|
||||
invitee.uuid,
|
||||
)
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(inviteeSubscription)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -5,16 +5,24 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { ActivatePremiumFeatures } from './ActivatePremiumFeatures'
|
||||
import { User } from '../../User/User'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
|
||||
describe('ActivatePremiumFeatures', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let subscriptionSettingsService: SubscriptionSettingServiceInterface
|
||||
let roleService: RoleServiceInterface
|
||||
let timer: TimerInterface
|
||||
let user: User
|
||||
|
||||
const createUseCase = () =>
|
||||
new ActivatePremiumFeatures(userRepository, userSubscriptionRepository, roleService, timer)
|
||||
new ActivatePremiumFeatures(
|
||||
userRepository,
|
||||
userSubscriptionRepository,
|
||||
subscriptionSettingsService,
|
||||
roleService,
|
||||
timer,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
@@ -32,6 +40,9 @@ describe('ActivatePremiumFeatures', () => {
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date('2024-01-01T00:00:00.000Z'))
|
||||
|
||||
subscriptionSettingsService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingsService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
})
|
||||
|
||||
it('should return error when username is invalid', async () => {
|
||||
@@ -64,4 +75,28 @@ describe('ActivatePremiumFeatures', () => {
|
||||
expect(userSubscriptionRepository.save).toHaveBeenCalled()
|
||||
expect(roleService.addUserRole).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save a subscription with custom plan name and endsAt', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: 'PRO_PLAN',
|
||||
endsAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
})
|
||||
|
||||
it('should fail when subscription plan name is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
username: 'test@test.te',
|
||||
subscriptionPlanName: 'some invalid plan name',
|
||||
endsAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,11 +7,14 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { ActivatePremiumFeaturesDTO } from './ActivatePremiumFeaturesDTO'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
private roleService: RoleServiceInterface,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
@@ -27,22 +30,35 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
if (user === null) {
|
||||
return Result.fail(`User not found with username: ${username.value}`)
|
||||
}
|
||||
const subscriptionPlanNameString = dto.subscriptionPlanName ?? SubscriptionPlanName.NAMES.ProPlan
|
||||
const subscriptionPlanNameOrError = SubscriptionPlanName.create(subscriptionPlanNameString)
|
||||
if (subscriptionPlanNameOrError.isFailed()) {
|
||||
return Result.fail(subscriptionPlanNameOrError.getError())
|
||||
}
|
||||
const subscriptionPlanName = subscriptionPlanNameOrError.getValue()
|
||||
|
||||
const timestamp = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
const endsAt = dto.endsAt ?? this.timer.getUTCDateNDaysAhead(365)
|
||||
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = SubscriptionPlanName.NAMES.ProPlan
|
||||
subscription.planName = subscriptionPlanName.value
|
||||
subscription.user = Promise.resolve(user)
|
||||
subscription.createdAt = timestamp
|
||||
subscription.updatedAt = timestamp
|
||||
subscription.endsAt = this.timer.convertDateToMicroseconds(this.timer.getUTCDateNDaysAhead(365))
|
||||
subscription.endsAt = this.timer.convertDateToMicroseconds(endsAt)
|
||||
subscription.cancelled = false
|
||||
subscription.subscriptionId = 1
|
||||
subscription.subscriptionType = UserSubscriptionType.Regular
|
||||
|
||||
await this.userSubscriptionRepository.save(subscription)
|
||||
|
||||
await this.roleService.addUserRole(user, SubscriptionPlanName.NAMES.ProPlan)
|
||||
await this.roleService.addUserRole(user, subscriptionPlanName.value)
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
subscription,
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
|
||||
)
|
||||
|
||||
return Result.ok('Premium features activated.')
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
endsAt?: Date
|
||||
}
|
||||
|
||||
@@ -268,6 +268,7 @@ describe('UpdateSetting', () => {
|
||||
serverEncryptionVersion: 1,
|
||||
sensitive: false,
|
||||
},
|
||||
user,
|
||||
userSubscription: regularSubscription,
|
||||
})
|
||||
|
||||
@@ -303,6 +304,7 @@ describe('UpdateSetting', () => {
|
||||
serverEncryptionVersion: 1,
|
||||
sensitive: false,
|
||||
},
|
||||
user,
|
||||
userSubscription: sharedSubscription,
|
||||
})
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ export class UpdateSetting implements UseCaseInterface {
|
||||
|
||||
const response = await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props,
|
||||
})
|
||||
|
||||
|
||||
@@ -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.23.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.22.0...@standardnotes/domain-core@1.23.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.21.1...@standardnotes/domain-core@1.22.0) (2023-07-12)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.22.0",
|
||||
"version": "1.23.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -4,10 +4,6 @@ import { UniqueEntityId } from '../Core/UniqueEntityId'
|
||||
import { CacheEntryProps } from './CacheEntryProps'
|
||||
|
||||
export class CacheEntry extends Entity<CacheEntryProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: CacheEntryProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { Entity } from './Entity'
|
||||
import { UniqueEntityId } from './UniqueEntityId'
|
||||
|
||||
export abstract class Aggregate<T> extends Entity<T> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
}
|
||||
export abstract class Aggregate<T> extends Entity<T> {}
|
||||
|
||||
@@ -9,6 +9,10 @@ export abstract class Entity<T> {
|
||||
this._id = id ? id : new UniqueEntityId()
|
||||
}
|
||||
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
public equals(object?: Entity<T>): boolean {
|
||||
if (object == null || object == undefined) {
|
||||
return false
|
||||
|
||||
@@ -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.7](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.6...@standardnotes/event-store@1.11.7) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.6](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.5...@standardnotes/event-store@1.11.6) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.6",
|
||||
"version": "1.11.7",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.19.9](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.8...@standardnotes/files-server@1.19.9) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.19.8](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.7...@standardnotes/files-server@1.19.8) (2023-07-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **files:** handling unlimited storage quota on home server ([9be3517](https://github.com/standardnotes/files/commit/9be3517093f8dd7bbdd7507c1e2ff059e6c9a889))
|
||||
|
||||
## [1.19.7](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.6...@standardnotes/files-server@1.19.7) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.19.6](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.5...@standardnotes/files-server@1.19.6) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.19.6",
|
||||
"version": "1.19.9",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -35,7 +35,7 @@
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/security": "workspace:*",
|
||||
"@standardnotes/sncrypto-common": "^1.9.0",
|
||||
"@standardnotes/sncrypto-common": "^1.13.4",
|
||||
"@standardnotes/sncrypto-node": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"connect-busboy": "^1.0.0",
|
||||
|
||||
@@ -135,4 +135,27 @@ describe('FinishUploadSession', () => {
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should ignore the storage quota if user has unlimited storage', async () => {
|
||||
uploadRepository.retrieveUploadChunkResults = jest.fn().mockReturnValue([
|
||||
{ tag: '123', chunkId: 1, chunkSize: 60 },
|
||||
{ tag: '234', chunkId: 2, chunkSize: 10 },
|
||||
{ tag: '345', chunkId: 3, chunkSize: 20 },
|
||||
])
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerUuid: '1-2-3',
|
||||
ownerType: 'user',
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 20,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(fileUploader.finishUploadSession).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,8 +43,9 @@ export class FinishUploadSession implements UseCaseInterface {
|
||||
totalFileSize += uploadChunkResult.chunkSize
|
||||
}
|
||||
|
||||
const userHasUnlimitedStorage = dto.uploadBytesLimit === -1
|
||||
const remainingSpaceLeft = dto.uploadBytesLimit - dto.uploadBytesUsed
|
||||
if (remainingSpaceLeft < totalFileSize) {
|
||||
if (!userHasUnlimitedStorage && remainingSpaceLeft < totalFileSize) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not finish upload session. You are out of space.',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
E2E_TESTING=false
|
||||
|
||||
JWT_SECRET=
|
||||
AUTH_JWT_SECRET=
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.4](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.12.3...@standardnotes/home-server@1.12.4) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.12.3](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.12.2...@standardnotes/home-server@1.12.3) (2023-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home-server:** allow custom atributtes for activating premium features ([f7190c0](https://github.com/standardnotes/server/commit/f7190c0c9c2d105f97d1cf980ce6a4f0dae34805))
|
||||
|
||||
## [1.12.2](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.12.1...@standardnotes/home-server@1.12.2) (2023-07-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.12.1](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.12.0...@standardnotes/home-server@1.12.1) (2023-07-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.41...@standardnotes/home-server@1.12.0) (2023-07-13)
|
||||
|
||||
### Features
|
||||
|
||||
* **home-server:** add activating premium features during an e2e test suite run ([2f0903e](https://github.com/standardnotes/server/commit/2f0903e0ebba95bcf0ece45045dc18e8a988b930))
|
||||
|
||||
## [1.11.41](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.40...@standardnotes/home-server@1.11.41) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.11.40](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.39...@standardnotes/home-server@1.11.40) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.11.39](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.11.38...@standardnotes/home-server@1.11.39) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.11.39",
|
||||
"version": "1.12.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -138,6 +138,22 @@ export class HomeServer implements HomeServerInterface {
|
||||
Disallow: '/',
|
||||
}),
|
||||
)
|
||||
|
||||
if (env.get('E2E_TESTING', true) === 'true') {
|
||||
app.post('/e2e/activate-premium', (request: Request, response: Response) => {
|
||||
void this.activatePremiumFeatures({
|
||||
username: request.body.username,
|
||||
subscriptionPlanName: request.body.subscriptionPlanName,
|
||||
endsAt: request.body.endsAt ? new Date(request.body.endsAt) : undefined,
|
||||
}).then((result) => {
|
||||
if (result.isFailed()) {
|
||||
response.status(400).send({ error: { message: result.getError() } })
|
||||
} else {
|
||||
response.status(200).send({ message: result.getValue() })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const logger: winston.Logger = winston.loggers.get('home-server')
|
||||
@@ -200,12 +216,16 @@ export class HomeServer implements HomeServerInterface {
|
||||
return this.serverInstance.address() !== null
|
||||
}
|
||||
|
||||
async activatePremiumFeatures(username: string): Promise<Result<string>> {
|
||||
async activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>> {
|
||||
if (!this.isRunning() || !this.authService) {
|
||||
return Result.fail('Home server is not running.')
|
||||
}
|
||||
|
||||
return this.authService.activatePremiumFeatures(username)
|
||||
return this.authService.activatePremiumFeatures(dto)
|
||||
}
|
||||
|
||||
private configureLoggers(env: Env, configuration: HomeServerConfiguration): void {
|
||||
|
||||
@@ -3,7 +3,11 @@ import { HomeServerConfiguration } from './HomeServerConfiguration'
|
||||
|
||||
export interface HomeServerInterface {
|
||||
start(configuration?: HomeServerConfiguration): Promise<Result<string>>
|
||||
activatePremiumFeatures(username: string): Promise<Result<string>>
|
||||
activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>>
|
||||
stop(): Promise<Result<string>>
|
||||
isRunning(): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.25.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.24.1...@standardnotes/revisions-server@1.25.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [1.24.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.24.0...@standardnotes/revisions-server@1.24.1) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
# [1.24.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.23.9...@standardnotes/revisions-server@1.24.0) (2023-07-12)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.24.0",
|
||||
"version": "1.25.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -27,12 +27,12 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.332.0",
|
||||
"@aws-sdk/client-sqs": "^3.332.0",
|
||||
"@standardnotes/api": "^1.26.25",
|
||||
"@standardnotes/api": "^1.26.26",
|
||||
"@standardnotes/common": "workspace:^",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/responses": "^1.13.9",
|
||||
"@standardnotes/responses": "^1.13.27",
|
||||
"@standardnotes/security": "workspace:^",
|
||||
"@standardnotes/time": "workspace:^",
|
||||
"cors": "2.8.5",
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { RevisionProps } from './RevisionProps'
|
||||
|
||||
export class Revision extends Entity<RevisionProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: RevisionProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { RevisionMetadataProps } from './RevisionMetadataProps'
|
||||
|
||||
export class RevisionMetadata extends Entity<RevisionMetadataProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: RevisionMetadataProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -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.9](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.8...@standardnotes/scheduler-server@1.20.9) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.8](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.7...@standardnotes/scheduler-server@1.20.8) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.8",
|
||||
"version": "1.20.9",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.14](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.13...@standardnotes/settings@1.21.14) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.13](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.12...@standardnotes/settings@1.21.13) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/settings",
|
||||
"version": "1.21.13",
|
||||
"version": "1.21.14",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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.15.3](https://github.com/standardnotes/server/compare/@standardnotes/sncrypto-node@1.15.2...@standardnotes/sncrypto-node@1.15.3) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/sncrypto-node
|
||||
|
||||
## [1.15.2](https://github.com/standardnotes/server/compare/@standardnotes/sncrypto-node@1.15.0...@standardnotes/sncrypto-node@1.15.2) (2023-06-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/sncrypto-node",
|
||||
"version": "1.15.2",
|
||||
"version": "1.15.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"test": "jest spec"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/sncrypto-common": "^1.9.0",
|
||||
"@standardnotes/sncrypto-common": "^1.13.4",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.64.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.63.1...@standardnotes/syncing-server@1.64.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/syncing-server-js/issues/659)) ([cb74b23](https://github.com/standardnotes/syncing-server-js/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [1.63.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.63.0...@standardnotes/syncing-server@1.63.1) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
# [1.63.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.62.1...@standardnotes/syncing-server@1.63.0) (2023-07-12)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.63.0",
|
||||
"version": "1.64.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -31,12 +31,12 @@
|
||||
"@aws-sdk/client-s3": "^3.332.0",
|
||||
"@aws-sdk/client-sns": "^3.332.0",
|
||||
"@aws-sdk/client-sqs": "^3.332.0",
|
||||
"@standardnotes/api": "^1.26.25",
|
||||
"@standardnotes/api": "^1.26.26",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/responses": "^1.13.9",
|
||||
"@standardnotes/responses": "^1.13.27",
|
||||
"@standardnotes/security": "workspace:*",
|
||||
"@standardnotes/settings": "workspace:*",
|
||||
"@standardnotes/sncrypto-node": "workspace:*",
|
||||
|
||||
@@ -23,8 +23,6 @@ import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { ItemTransferCalculatorInterface } from '../Domain/Item/ItemTransferCalculatorInterface'
|
||||
import { ItemTransferCalculator } from '../Domain/Item/ItemTransferCalculator'
|
||||
import { ItemConflict } from '../Domain/Item/ItemConflict'
|
||||
import { ItemService } from '../Domain/Item/ItemService'
|
||||
import { ItemServiceInterface } from '../Domain/Item/ItemServiceInterface'
|
||||
import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter'
|
||||
import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter'
|
||||
import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter'
|
||||
@@ -75,6 +73,11 @@ import { ItemBackupRepresentation } from '../Mapping/Backup/ItemBackupRepresenta
|
||||
import { ItemBackupMapper } from '../Mapping/Backup/ItemBackupMapper'
|
||||
import { SaveNewItem } from '../Domain/UseCase/Syncing/SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
|
||||
import { GetItems } from '../Domain/UseCase/Syncing/GetItems/GetItems'
|
||||
import { SaveItems } from '../Domain/UseCase/Syncing/SaveItems/SaveItems'
|
||||
import { ItemHashHttpMapper } from '../Mapping/Http/ItemHashHttpMapper'
|
||||
import { ItemHash } from '../Domain/Item/ItemHash'
|
||||
import { ItemHashHttpRepresentation } from '../Mapping/Http/ItemHashHttpRepresentation'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -209,6 +212,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
|
||||
.toConstantValue(new ItemPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<ItemHash, ItemHashHttpRepresentation>>(TYPES.Sync_ItemHashHttpMapper)
|
||||
.toConstantValue(new ItemHashHttpMapper())
|
||||
container
|
||||
.bind<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper)
|
||||
.toConstantValue(new ItemHttpMapper(container.get(TYPES.Sync_Timer)))
|
||||
@@ -217,7 +223,12 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(new SavedItemHttpMapper(container.get(TYPES.Sync_Timer)))
|
||||
container
|
||||
.bind<MapperInterface<ItemConflict, ItemConflictHttpRepresentation>>(TYPES.Sync_ItemConflictHttpMapper)
|
||||
.toConstantValue(new ItemConflictHttpMapper(container.get(TYPES.Sync_ItemHttpMapper)))
|
||||
.toConstantValue(
|
||||
new ItemConflictHttpMapper(
|
||||
container.get(TYPES.Sync_ItemHttpMapper),
|
||||
container.get(TYPES.Sync_ItemHashHttpMapper),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
|
||||
.toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
|
||||
@@ -282,16 +293,35 @@ export class ContainerConfigLoader {
|
||||
env.get('MAX_ITEMS_LIMIT', true) ? +env.get('MAX_ITEMS_LIMIT', true) : this.DEFAULT_MAX_ITEMS_LIMIT,
|
||||
)
|
||||
|
||||
container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toConstantValue(new OwnershipFilter())
|
||||
container
|
||||
.bind<TimeDifferenceFilter>(TYPES.Sync_TimeDifferenceFilter)
|
||||
.toConstantValue(new TimeDifferenceFilter(container.get(TYPES.Sync_Timer)))
|
||||
container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toConstantValue(new ContentTypeFilter())
|
||||
container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toConstantValue(new ContentFilter())
|
||||
container
|
||||
.bind<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
|
||||
.toConstantValue(
|
||||
new ItemSaveValidator([
|
||||
container.get(TYPES.Sync_OwnershipFilter),
|
||||
container.get(TYPES.Sync_TimeDifferenceFilter),
|
||||
container.get(TYPES.Sync_ContentTypeFilter),
|
||||
container.get(TYPES.Sync_ContentFilter),
|
||||
]),
|
||||
)
|
||||
|
||||
// use cases
|
||||
container.bind<SyncItems>(TYPES.Sync_SyncItems).toDynamicValue((context: interfaces.Context) => {
|
||||
return new SyncItems(context.container.get(TYPES.Sync_ItemService))
|
||||
})
|
||||
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
container
|
||||
.bind<GetItems>(TYPES.Sync_GetItems)
|
||||
.toConstantValue(
|
||||
new GetItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SaveNewItem>(TYPES.Sync_SaveNewItem)
|
||||
.toConstantValue(
|
||||
@@ -313,41 +343,35 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_REVISIONS_FREQUENCY),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toConstantValue(new OwnershipFilter())
|
||||
container
|
||||
.bind<TimeDifferenceFilter>(TYPES.Sync_TimeDifferenceFilter)
|
||||
.toConstantValue(new TimeDifferenceFilter(container.get(TYPES.Sync_Timer)))
|
||||
container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toConstantValue(new ContentTypeFilter())
|
||||
container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toConstantValue(new ContentFilter())
|
||||
|
||||
container
|
||||
.bind<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemSaveValidator([
|
||||
context.container.get(TYPES.Sync_OwnershipFilter),
|
||||
context.container.get(TYPES.Sync_TimeDifferenceFilter),
|
||||
context.container.get(TYPES.Sync_ContentTypeFilter),
|
||||
context.container.get(TYPES.Sync_ContentFilter),
|
||||
])
|
||||
})
|
||||
|
||||
container
|
||||
.bind<ItemServiceInterface>(TYPES.Sync_ItemService)
|
||||
.bind<SaveItems>(TYPES.Sync_SaveItems)
|
||||
.toConstantValue(
|
||||
new ItemService(
|
||||
new SaveItems(
|
||||
container.get(TYPES.Sync_ItemSaveValidator),
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
|
||||
container.get(TYPES.Sync_SaveNewItem),
|
||||
container.get(TYPES.Sync_UpdateExistingItem),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SyncItems>(TYPES.Sync_SyncItems)
|
||||
.toConstantValue(
|
||||
new SyncItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_GetItems),
|
||||
container.get(TYPES.Sync_SaveItems),
|
||||
),
|
||||
)
|
||||
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
|
||||
// Services
|
||||
container
|
||||
.bind<SyncResponseFactory20161215>(TYPES.Sync_SyncResponseFactory20161215)
|
||||
.toConstantValue(new SyncResponseFactory20161215(container.get(TYPES.Sync_ItemHttpMapper)))
|
||||
|
||||
@@ -55,6 +55,8 @@ const TYPES = {
|
||||
Sync_DeleteMessage: Symbol.for('Sync_DeleteMessage'),
|
||||
Sync_SaveNewItem: Symbol.for('Sync_SaveNewItem'),
|
||||
Sync_UpdateExistingItem: Symbol.for('Sync_UpdateExistingItem'),
|
||||
Sync_GetItems: Symbol.for('Sync_GetItems'),
|
||||
Sync_SaveItems: Symbol.for('Sync_SaveItems'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
@@ -67,7 +69,6 @@ const TYPES = {
|
||||
Sync_DomainEventFactory: Symbol.for('Sync_DomainEventFactory'),
|
||||
Sync_DomainEventMessageHandler: Symbol.for('Sync_DomainEventMessageHandler'),
|
||||
Sync_HTTPClient: Symbol.for('Sync_HTTPClient'),
|
||||
Sync_ItemService: Symbol.for('Sync_ItemService'),
|
||||
Sync_Timer: Symbol.for('Sync_Timer'),
|
||||
Sync_SyncResponseFactory20161215: Symbol.for('Sync_SyncResponseFactory20161215'),
|
||||
Sync_SyncResponseFactory20200115: Symbol.for('Sync_SyncResponseFactory20200115'),
|
||||
@@ -90,6 +91,7 @@ const TYPES = {
|
||||
Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
|
||||
Sync_ItemPersistenceMapper: Symbol.for('Sync_ItemPersistenceMapper'),
|
||||
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'),
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Item } from './Item'
|
||||
|
||||
export type GetItemsResult = {
|
||||
items: Array<Item>
|
||||
cursorToken?: string
|
||||
}
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { ItemProps } from './ItemProps'
|
||||
|
||||
export class Item extends Entity<ItemProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: ItemProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
export type ItemHash = {
|
||||
uuid: string
|
||||
content?: string
|
||||
content_type: string | null
|
||||
deleted?: boolean
|
||||
duplicate_of?: string | null
|
||||
auth_hash?: string
|
||||
enc_item_key?: string
|
||||
items_key_id?: string
|
||||
created_at?: string
|
||||
created_at_timestamp?: number
|
||||
updated_at?: string
|
||||
updated_at_timestamp?: number
|
||||
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemHashProps } from './ItemHashProps'
|
||||
|
||||
export class ItemHash extends ValueObject<ItemHashProps> {
|
||||
private constructor(props: ItemHashProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(props: ItemHashProps): Result<ItemHash> {
|
||||
return Result.ok<ItemHash>(new ItemHash(props))
|
||||
}
|
||||
}
|
||||
|
||||
17
packages/syncing-server/src/Domain/Item/ItemHashProps.ts
Normal file
17
packages/syncing-server/src/Domain/Item/ItemHashProps.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface ItemHashProps {
|
||||
uuid: string
|
||||
user_uuid: string
|
||||
content?: string
|
||||
content_type: string | null
|
||||
deleted?: boolean
|
||||
duplicate_of?: string | null
|
||||
auth_hash?: string
|
||||
enc_item_key?: string
|
||||
items_key_id?: string
|
||||
key_system_identifier: string | null
|
||||
shared_vault_uuid: string | null
|
||||
created_at?: string
|
||||
created_at_timestamp?: number
|
||||
updated_at?: string
|
||||
updated_at_timestamp?: number
|
||||
}
|
||||
@@ -1,785 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { Item } from './Item'
|
||||
import { ItemHash } from './ItemHash'
|
||||
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
import { ItemService } from './ItemService'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
|
||||
import { ItemConflict } from './ItemConflict'
|
||||
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
|
||||
import { SaveNewItem } from '../UseCase/Syncing/SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('ItemService', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
const contentSizeTransferLimit = 100
|
||||
let timer: TimerInterface
|
||||
let item1: Item
|
||||
let item2: Item
|
||||
let itemHash1: ItemHash
|
||||
let itemHash2: ItemHash
|
||||
let syncToken: string
|
||||
let logger: Logger
|
||||
let itemSaveValidator: ItemSaveValidatorInterface
|
||||
let newItem: Item
|
||||
let timeHelper: Timer
|
||||
let itemTransferCalculator: ItemTransferCalculatorInterface
|
||||
let saveNewItemUseCase: SaveNewItem
|
||||
let updateExistingItemUseCase: UpdateExistingItem
|
||||
const maxItemsSyncLimit = 300
|
||||
|
||||
const createService = () =>
|
||||
new ItemService(
|
||||
itemSaveValidator,
|
||||
itemRepository,
|
||||
contentSizeTransferLimit,
|
||||
itemTransferCalculator,
|
||||
timer,
|
||||
maxItemsSyncLimit,
|
||||
saveNewItemUseCase,
|
||||
updateExistingItemUseCase,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
timeHelper = new Timer()
|
||||
|
||||
item1 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
item2 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
itemHash1 = {
|
||||
uuid: '1-2-3',
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe1',
|
||||
items_key_id: 'asdasd1',
|
||||
created_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
itemHash2 = {
|
||||
uuid: '2-3-4',
|
||||
content: 'asdqwe2',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe2',
|
||||
items_key_id: 'asdasd2',
|
||||
created_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(item2.props.timestamps.createdAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item2.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest
|
||||
.fn()
|
||||
.mockReturnValue([item1.id.toString(), item2.id.toString()])
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item1, item2])
|
||||
itemRepository.countAll = jest.fn().mockReturnValue(2)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date())
|
||||
timer.convertStringDateToDate = jest
|
||||
.fn()
|
||||
.mockImplementation((date: string) => timeHelper.convertStringDateToDate(date))
|
||||
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(600)
|
||||
timer.convertStringDateToMicroseconds = jest
|
||||
.fn()
|
||||
.mockImplementation((date: string) => timeHelper.convertStringDateToMicroseconds(date))
|
||||
timer.convertMicrosecondsToDate = jest
|
||||
.fn()
|
||||
.mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
|
||||
syncToken = Buffer.from('2:1616164633.241564', 'utf-8').toString('base64')
|
||||
|
||||
itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
|
||||
itemSaveValidator.validate = jest.fn().mockReturnValue({ passed: true })
|
||||
|
||||
newItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241313), new Date(1616164633241313)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241313, 1616164633241313).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
|
||||
).getValue()
|
||||
|
||||
saveNewItemUseCase = {} as jest.Mocked<SaveNewItem>
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(newItem))
|
||||
|
||||
updateExistingItemUseCase = {} as jest.Mocked<UpdateExistingItem>
|
||||
updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(item1))
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from last sync with sync token version 1', async () => {
|
||||
syncToken = Buffer.from('1:2021-03-15 07:00:00', 'utf-8').toString('base64')
|
||||
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1615791600000000,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortOrder: 'ASC',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from last sync', async () => {
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from last sync with upper bound items limit', async () => {
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
limit: 1000,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 300,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve no items for a user if there are none from last sync', async () => {
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockReturnValue([])
|
||||
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [],
|
||||
})
|
||||
|
||||
expect(itemRepository.findAll).not.toHaveBeenCalled()
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a cursor token if there are more items than requested with limit', async () => {
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item1])
|
||||
|
||||
const itemsResponse = await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
limit: 1,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
|
||||
expect(itemsResponse).toEqual({
|
||||
cursorToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMQ==',
|
||||
items: [item1],
|
||||
})
|
||||
|
||||
expect(Buffer.from(<string>itemsResponse.cursorToken, 'base64').toString('utf-8')).toEqual('2:1616164633.241311')
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 1,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from cursor token', async () => {
|
||||
const cursorToken = Buffer.from('2:1616164633.241123', 'utf-8').toString('base64')
|
||||
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
cursorToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241123,
|
||||
syncTimeComparison: '>=',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all undeleted items for a user without cursor or sync token', async () => {
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
deleted: false,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
syncTimeComparison: '>',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items with default limit if not defined', async () => {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortOrder: 'ASC',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items with non-positive limit if not defined', async () => {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
limit: 0,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the sync token is missing time', async () => {
|
||||
let error = null
|
||||
|
||||
try {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken: '2:',
|
||||
limit: 0,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw an error if the sync token is missing version', async () => {
|
||||
let error = null
|
||||
|
||||
try {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken: '1234567890',
|
||||
limit: 0,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should front load keys items to top of the collection for better client performance', async () => {
|
||||
const item3 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue()
|
||||
const item4 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000004'),
|
||||
).getValue()
|
||||
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item3, item4])
|
||||
|
||||
await createService().frontLoadKeysItemsToTop('1-2-3', [item1, item2])
|
||||
})
|
||||
|
||||
it('should save new items', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [newItem],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxNA==',
|
||||
})
|
||||
|
||||
expect(saveNewItemUseCase.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not save new items in read only access mode', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: true,
|
||||
sessionUuid: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'readonly_error',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
|
||||
expect(saveNewItemUseCase.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save new items that are duplicates', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
const duplicateItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241570), new Date(1616164633241570)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241570, 1616164633241570).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000005'),
|
||||
).getValue()
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(duplicateItem))
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [duplicateItem],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU3MQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip items that are conflicting on validation', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const conflict = {} as jest.Mocked<ItemConflict>
|
||||
const validationResult = { passed: false, conflict }
|
||||
itemSaveValidator.validate = jest.fn().mockReturnValue(validationResult)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [conflict],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark items as saved that are skipped on validation', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const skipped = item1
|
||||
const validationResult = { passed: false, skipped }
|
||||
itemSaveValidator.validate = jest.fn().mockReturnValue(validationResult)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [skipped],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMg==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should calculate the sync token based on last updated date of saved items incremented with 1 microsecond to avoid returning same object in subsequent sync', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const itemHash3 = {
|
||||
uuid: '3-4-5',
|
||||
content: 'asdqwe3',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe3',
|
||||
items_key_id: 'asdasd3',
|
||||
created_at: '2021-02-19T11:35:45.652Z',
|
||||
updated_at: '2021-03-25T09:37:37.943Z',
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const saveProcedureStartTimestamp = 1616164633241580
|
||||
const item1Timestamp = 1616164633241570
|
||||
const item2Timestamp = 1616164633241568
|
||||
const item3Timestamp = 1616164633241569
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValueOnce(saveProcedureStartTimestamp)
|
||||
|
||||
saveNewItemUseCase.execute = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item1.props,
|
||||
timestamps: Timestamps.create(item1Timestamp, item1Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item2.props,
|
||||
timestamps: Timestamps.create(item2Timestamp, item2Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item2.props,
|
||||
timestamps: Timestamps.create(item3Timestamp, item3Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1, itemHash3, itemHash2],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result.syncToken).toEqual('MjoxNjE2MTY0NjMzLjI0MTU3MQ==')
|
||||
expect(Buffer.from(result.syncToken, 'base64').toString('utf-8')).toEqual('2:1616164633.241571')
|
||||
})
|
||||
|
||||
it('should update existing items', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [item1],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMg==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark as skipped existing items that failed to update', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
|
||||
updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip saving conflicting items and mark them as sync conflicts when saving fails', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1, itemHash2],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash2,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip saving conflicting items and mark them as sync conflicts when saving throws an error', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
saveNewItemUseCase.execute = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1, itemHash2],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash2,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,239 +0,0 @@
|
||||
import { Time, TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { GetItemsDTO } from './GetItemsDTO'
|
||||
import { GetItemsResult } from './GetItemsResult'
|
||||
import { Item } from './Item'
|
||||
import { ItemConflict } from './ItemConflict'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
import { ItemServiceInterface } from './ItemServiceInterface'
|
||||
import { SaveItemsDTO } from './SaveItemsDTO'
|
||||
import { SaveItemsResult } from './SaveItemsResult'
|
||||
import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
|
||||
import { SaveNewItem } from '../UseCase/Syncing/SaveNewItem/SaveNewItem'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { UpdateExistingItem } from '../UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
|
||||
|
||||
export class ItemService implements ItemServiceInterface {
|
||||
private readonly DEFAULT_ITEMS_LIMIT = 150
|
||||
private readonly SYNC_TOKEN_VERSION = 2
|
||||
|
||||
constructor(
|
||||
private itemSaveValidator: ItemSaveValidatorInterface,
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private contentSizeTransferLimit: number,
|
||||
private itemTransferCalculator: ItemTransferCalculatorInterface,
|
||||
private timer: TimerInterface,
|
||||
private maxItemsSyncLimit: number,
|
||||
private saveNewItem: SaveNewItem,
|
||||
private updateExistingItem: UpdateExistingItem,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async getItems(dto: GetItemsDTO): Promise<GetItemsResult> {
|
||||
const lastSyncTime = this.getLastSyncTime(dto)
|
||||
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 itemQuery: ItemQuery = {
|
||||
userUuid: dto.userUuid,
|
||||
lastSyncTime,
|
||||
syncTimeComparison,
|
||||
contentType: dto.contentType,
|
||||
deleted: lastSyncTime ? undefined : false,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
limit: upperBoundLimit,
|
||||
}
|
||||
|
||||
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
||||
itemQuery,
|
||||
this.contentSizeTransferLimit,
|
||||
)
|
||||
let items: Array<Item> = []
|
||||
if (itemUuidsToFetch.length > 0) {
|
||||
items = await this.itemRepository.findAll({
|
||||
uuids: itemUuidsToFetch,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
}
|
||||
const totalItemsCount = await this.itemRepository.countAll(itemQuery)
|
||||
|
||||
let cursorToken = undefined
|
||||
if (totalItemsCount > upperBoundLimit) {
|
||||
const lastSyncTime = items[items.length - 1].props.timestamps.updatedAt / Time.MicrosecondsInASecond
|
||||
cursorToken = Buffer.from(`${this.SYNC_TOKEN_VERSION}:${lastSyncTime}`, 'utf-8').toString('base64')
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
cursorToken,
|
||||
}
|
||||
}
|
||||
|
||||
async saveItems(dto: SaveItemsDTO): Promise<SaveItemsResult> {
|
||||
const savedItems: Array<Item> = []
|
||||
const conflicts: Array<ItemConflict> = []
|
||||
|
||||
const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
for (const itemHash of dto.itemHashes) {
|
||||
if (dto.readOnlyAccess) {
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.ReadOnlyError,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const existingItem = await this.itemRepository.findByUuid(itemHash.uuid)
|
||||
const processingResult = await this.itemSaveValidator.validate({
|
||||
userUuid: dto.userUuid,
|
||||
apiVersion: dto.apiVersion,
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
if (!processingResult.passed) {
|
||||
if (processingResult.conflict) {
|
||||
conflicts.push(processingResult.conflict)
|
||||
}
|
||||
if (processingResult.skipped) {
|
||||
savedItems.push(processingResult.skipped)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (existingItem) {
|
||||
const udpatedItemOrError = await this.updateExistingItem.execute({
|
||||
existingItem,
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
})
|
||||
if (udpatedItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Updating item ${itemHash.uuid} failed. Error: ${udpatedItemOrError.getError()}`,
|
||||
)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
const updatedItem = udpatedItemOrError.getValue()
|
||||
|
||||
savedItems.push(updatedItem)
|
||||
} else {
|
||||
try {
|
||||
const newItemOrError = await this.saveNewItem.execute({
|
||||
userUuid: dto.userUuid,
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
})
|
||||
if (newItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Saving item ${itemHash.uuid} failed. Error: ${newItemOrError.getError()}`,
|
||||
)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
const newItem = newItemOrError.getValue()
|
||||
|
||||
savedItems.push(newItem)
|
||||
} catch (error) {
|
||||
this.logger.error(`[${dto.userUuid}] Saving item ${itemHash.uuid} failed. Error: ${(error as Error).message}`)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncToken = this.calculateSyncToken(lastUpdatedTimestamp, savedItems)
|
||||
|
||||
return {
|
||||
savedItems,
|
||||
conflicts,
|
||||
syncToken,
|
||||
}
|
||||
}
|
||||
|
||||
async frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>> {
|
||||
const itemsKeys = await this.itemRepository.findAll({
|
||||
userUuid,
|
||||
contentType: ContentType.TYPES.ItemsKey,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
|
||||
const retrievedItemsIds: Array<string> = retrievedItems.map((item: Item) => item.id.toString())
|
||||
|
||||
itemsKeys.forEach((itemKey: Item) => {
|
||||
if (retrievedItemsIds.indexOf(itemKey.id.toString()) === -1) {
|
||||
retrievedItems.unshift(itemKey)
|
||||
}
|
||||
})
|
||||
|
||||
return retrievedItems
|
||||
}
|
||||
|
||||
private calculateSyncToken(lastUpdatedTimestamp: number, savedItems: Array<Item>): string {
|
||||
if (savedItems.length) {
|
||||
const sortedItems = savedItems.sort((itemA: Item, itemB: Item) => {
|
||||
return itemA.props.timestamps.updatedAt > itemB.props.timestamps.updatedAt ? 1 : -1
|
||||
})
|
||||
lastUpdatedTimestamp = sortedItems[sortedItems.length - 1].props.timestamps.updatedAt
|
||||
}
|
||||
|
||||
const lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles = lastUpdatedTimestamp + 1
|
||||
|
||||
return Buffer.from(
|
||||
`${this.SYNC_TOKEN_VERSION}:${
|
||||
lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles / Time.MicrosecondsInASecond
|
||||
}`,
|
||||
'utf-8',
|
||||
).toString('base64')
|
||||
}
|
||||
|
||||
private getLastSyncTime(dto: GetItemsDTO): number | undefined {
|
||||
let token = dto.syncToken
|
||||
if (dto.cursorToken !== undefined && dto.cursorToken !== null) {
|
||||
token = dto.cursorToken
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const decodedToken = Buffer.from(token, 'base64').toString('utf-8')
|
||||
|
||||
const tokenParts = decodedToken.split(':')
|
||||
const version = tokenParts.shift()
|
||||
|
||||
switch (version) {
|
||||
case '1':
|
||||
return this.timer.convertStringDateToMicroseconds(tokenParts.join(':'))
|
||||
case '2':
|
||||
return +tokenParts[0] * Time.MicrosecondsInASecond
|
||||
default:
|
||||
throw Error('Sync token is missing version part')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { GetItemsDTO } from './GetItemsDTO'
|
||||
import { GetItemsResult } from './GetItemsResult'
|
||||
import { Item } from './Item'
|
||||
import { SaveItemsDTO } from './SaveItemsDTO'
|
||||
import { SaveItemsResult } from './SaveItemsResult'
|
||||
|
||||
export interface ItemServiceInterface {
|
||||
getItems(dto: GetItemsDTO): Promise<GetItemsResult>
|
||||
saveItems(dto: SaveItemsDTO): Promise<SaveItemsResult>
|
||||
frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>>
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Item } from './Item'
|
||||
import { ItemConflict } from './ItemConflict'
|
||||
|
||||
export type SaveItemsResult = {
|
||||
savedItems: Array<Item>
|
||||
conflicts: Array<ItemConflict>
|
||||
syncToken: string
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Item } from '../Item'
|
||||
|
||||
import { ContentFilter } from './ContentFilter'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ItemHash } from '../ItemHash'
|
||||
|
||||
describe('ContentFilter', () => {
|
||||
let existingItem: Item
|
||||
@@ -14,25 +15,25 @@ describe('ContentFilter', () => {
|
||||
const invalidContents = [[], { foo: 'bar' }, [{ foo: 'bar' }], 123, new Date(1)]
|
||||
|
||||
for (const invalidContent of invalidContents) {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: invalidContent as unknown as string,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: invalidContent as unknown as string,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
itemHash,
|
||||
existingItem: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: invalidContent,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'content_error',
|
||||
},
|
||||
})
|
||||
@@ -46,11 +47,14 @@ describe('ContentFilter', () => {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
itemHash: ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: validContent as unknown as string,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue(),
|
||||
existingItem,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ import { ConflictType } from '@standardnotes/responses'
|
||||
|
||||
export class ContentFilter implements ItemSaveRuleInterface {
|
||||
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
|
||||
if (dto.itemHash.content === undefined || dto.itemHash.content === null) {
|
||||
if (dto.itemHash.props.content === undefined || dto.itemHash.props.content === null) {
|
||||
return {
|
||||
passed: true,
|
||||
}
|
||||
}
|
||||
|
||||
const validContent = typeof dto.itemHash.content === 'string'
|
||||
const validContent = typeof dto.itemHash.props.content === 'string'
|
||||
|
||||
if (!validContent) {
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ApiVersion } from '../../Api/ApiVersion'
|
||||
import { Item } from '../Item'
|
||||
|
||||
import { ContentTypeFilter } from './ContentTypeFilter'
|
||||
import { ItemHash } from '../ItemHash'
|
||||
|
||||
describe('ContentTypeFilter', () => {
|
||||
let existingItem: Item
|
||||
@@ -22,23 +23,24 @@ describe('ContentTypeFilter', () => {
|
||||
]
|
||||
|
||||
for (const invalidContentType of invalidContentTypes) {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: invalidContentType,
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: invalidContentType,
|
||||
},
|
||||
itemHash,
|
||||
existingItem: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: invalidContentType,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'content_type_error',
|
||||
},
|
||||
})
|
||||
@@ -49,13 +51,18 @@ describe('ContentTypeFilter', () => {
|
||||
const validContentTypes = ['Note', 'SN|ItemsKey', 'SN|Component', 'SN|Editor', 'SN|ExtensionRepo', 'Tag']
|
||||
|
||||
for (const validContentType of validContentTypes) {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: validContentType,
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: validContentType,
|
||||
},
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
|
||||
|
||||
export class ContentTypeFilter implements ItemSaveRuleInterface {
|
||||
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.props.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return {
|
||||
passed: false,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Item } from '../Item'
|
||||
|
||||
import { OwnershipFilter } from './OwnershipFilter'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { ItemHash } from '../ItemHash'
|
||||
|
||||
describe('OwnershipFilter', () => {
|
||||
let existingItem: Item
|
||||
@@ -30,23 +31,29 @@ describe('OwnershipFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items belonging to a different user', async () => {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000001',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'uuid_conflict',
|
||||
},
|
||||
})
|
||||
@@ -56,10 +63,18 @@ describe('OwnershipFilter', () => {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
itemHash: ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue(),
|
||||
existingItem,
|
||||
})
|
||||
|
||||
@@ -72,10 +87,18 @@ describe('OwnershipFilter', () => {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
itemHash: ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue(),
|
||||
existingItem: null,
|
||||
})
|
||||
|
||||
@@ -85,23 +108,29 @@ describe('OwnershipFilter', () => {
|
||||
})
|
||||
|
||||
it('should return an error if the user uuid is invalid', async () => {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: 'invalid',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'uuid_error',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -42,8 +42,11 @@ describe('TimeDifferenceFilter', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash = {
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '1-2-3',
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
@@ -57,7 +60,7 @@ describe('TimeDifferenceFilter', () => {
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
}).getValue()
|
||||
})
|
||||
|
||||
it('should leave non existing items', async () => {
|
||||
@@ -74,8 +77,11 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items from legacy clients', async () => {
|
||||
delete itemHash.updated_at
|
||||
delete itemHash.updated_at_timestamp
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: undefined,
|
||||
updated_at_timestamp: undefined,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -90,7 +96,10 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items having update at timestamp different in microseconds precision', async () => {
|
||||
itemHash.updated_at_timestamp = existingItem.props.timestamps.updatedAt + 1
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at_timestamp: existingItem.props.timestamps.updatedAt + 1,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -109,7 +118,10 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items having update at timestamp same in microseconds precision', async () => {
|
||||
itemHash.updated_at_timestamp = existingItem.props.timestamps.updatedAt
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at_timestamp: existingItem.props.timestamps.updatedAt,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -124,14 +136,17 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items having update at timestamp different by a second for legacy clients', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInASecond +
|
||||
1,
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInASecond +
|
||||
1,
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -150,10 +165,13 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items having update at timestamp different by less then a second for legacy clients', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -168,14 +186,17 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items having update at timestamp different by a millisecond', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInAMillisecond +
|
||||
1,
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInAMillisecond +
|
||||
1,
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -194,10 +215,13 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items having update at timestamp different by less than a millisecond', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
|
||||
@@ -17,11 +17,11 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface {
|
||||
}
|
||||
}
|
||||
|
||||
let incomingUpdatedAtTimestamp = dto.itemHash.updated_at_timestamp
|
||||
let incomingUpdatedAtTimestamp = dto.itemHash.props.updated_at_timestamp
|
||||
if (incomingUpdatedAtTimestamp === undefined) {
|
||||
incomingUpdatedAtTimestamp =
|
||||
dto.itemHash.updated_at !== undefined
|
||||
? this.timer.convertStringDateToMicroseconds(dto.itemHash.updated_at)
|
||||
dto.itemHash.props.updated_at !== undefined
|
||||
? this.timer.convertStringDateToMicroseconds(dto.itemHash.props.updated_at)
|
||||
: this.timer.convertStringDateToMicroseconds(new Date(0).toString())
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface {
|
||||
}
|
||||
|
||||
private itemHashHasMicrosecondsPrecision(itemHash: ItemHash) {
|
||||
return itemHash.updated_at_timestamp !== undefined
|
||||
return itemHash.props.updated_at_timestamp !== undefined
|
||||
}
|
||||
|
||||
private getMinimalConflictIntervalMicroseconds(apiVersion?: string): number {
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { MessageProps } from './MessageProps'
|
||||
|
||||
export class Message extends Entity<MessageProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: MessageProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { NotificationProps } from './NotificationProps'
|
||||
|
||||
export class Notification extends Entity<NotificationProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: NotificationProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItem } from './SharedVaultItem'
|
||||
|
||||
describe('SharedVaultItem', () => {
|
||||
it('should create an entity', () => {
|
||||
const entityOrError = SharedVaultItem.create({
|
||||
itemId: new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
sharedVaultId: new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123456789, 123456789).getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().id).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItemProps } from './SharedVaultItemProps'
|
||||
|
||||
export class SharedVaultItem extends Entity<SharedVaultItemProps> {
|
||||
private constructor(props: SharedVaultItemProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
static create(props: SharedVaultItemProps, id?: UniqueEntityId): Result<SharedVaultItem> {
|
||||
return Result.ok<SharedVaultItem>(new SharedVaultItem(props, id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface SharedVaultItemProps {
|
||||
sharedVaultId: UniqueEntityId
|
||||
itemId: UniqueEntityId
|
||||
keySystemIdentifier: string
|
||||
lastEditedBy: Uuid
|
||||
timestamps: Timestamps
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItem } from './SharedVaultItem'
|
||||
|
||||
export interface SharedVaultItemRepositoryInterface {
|
||||
save(sharedVaultItem: SharedVaultItem): Promise<void>
|
||||
findBySharedVaultId(sharedVaultId: UniqueEntityId): Promise<SharedVaultItem[]>
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user