feat: add models package
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ packages/filepicker/dist
|
||||
packages/features/dist
|
||||
packages/encryption/dist
|
||||
packages/files/dist
|
||||
packages/models/dist
|
||||
|
||||
**/.pnp.*
|
||||
**/.yarn/*
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.23.1",
|
||||
"@standardnotes/models": "^1.11.13",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "^1.6.39",
|
||||
"@standardnotes/services": "^1.13.23",
|
||||
"@standardnotes/sncrypto-common": "^1.9.0",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@standardnotes/common": "^1.23.1",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/filepicker": "workspace:*",
|
||||
"@standardnotes/models": "^1.11.13",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "^1.6.39",
|
||||
"@standardnotes/services": "^1.13.23",
|
||||
"@standardnotes/sncrypto-common": "^1.9.0",
|
||||
|
||||
2
packages/models/.eslintignore
Normal file
2
packages/models/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
10
packages/models/.eslintrc
Normal file
10
packages/models/.eslintrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": ["warn", { "ignoreRestArgs": true }],
|
||||
"@typescript-eslint/no-non-null-assertion": "warn"
|
||||
}
|
||||
}
|
||||
314
packages/models/CHANGELOG.md
Normal file
314
packages/models/CHANGELOG.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.14...@standardnotes/models@1.12.0) (2022-07-05)
|
||||
|
||||
### Features
|
||||
|
||||
* remove features package in favor of standardnotes/app repository ([bb8226b](https://github.com/standardnotes/snjs/commit/bb8226b77550707c2a981778a78fe3dccf1aaa03))
|
||||
|
||||
## [1.11.14](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.13...@standardnotes/models@1.11.14) (2022-07-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing reflect-metadata package to all packages ([ce3a5bb](https://github.com/standardnotes/snjs/commit/ce3a5bbf3f1d2276ac4abc3eec3c6a44c8c3ba9b))
|
||||
|
||||
## [1.11.13](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.12...@standardnotes/models@1.11.13) (2022-06-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.12](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.11...@standardnotes/models@1.11.12) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.11](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.10...@standardnotes/models@1.11.11) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.10](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.9...@standardnotes/models@1.11.10) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.9](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.8...@standardnotes/models@1.11.9) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.8](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.7...@standardnotes/models@1.11.8) (2022-06-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.7](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.6...@standardnotes/models@1.11.7) (2022-06-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.6](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.5...@standardnotes/models@1.11.6) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.5](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.4...@standardnotes/models@1.11.5) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.4](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.3...@standardnotes/models@1.11.4) (2022-06-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reverse title sort ([#757](https://github.com/standardnotes/snjs/issues/757)) ([dacee77](https://github.com/standardnotes/snjs/commit/dacee77488593ec71c670c1bfa62cc7f526c8b56))
|
||||
|
||||
## [1.11.3](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.2...@standardnotes/models@1.11.3) (2022-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* define getters on items used in predicates so keypath lookups are not undefined ([#756](https://github.com/standardnotes/snjs/issues/756)) ([3297077](https://github.com/standardnotes/snjs/commit/32970774897a48fd9a12b329ca204ed6882a47ab))
|
||||
|
||||
## [1.11.2](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.1...@standardnotes/models@1.11.2) (2022-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.11.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.11.0...@standardnotes/models@1.11.1) (2022-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.10.3...@standardnotes/models@1.11.0) (2022-05-27)
|
||||
|
||||
### Features
|
||||
|
||||
* add 'name' and 'offlineOnly' setters to ComponentMutator ([#751](https://github.com/standardnotes/snjs/issues/751)) ([55b1f68](https://github.com/standardnotes/snjs/commit/55b1f687fb25facf925b081871152e4ea7723886))
|
||||
|
||||
## [1.10.3](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.10.2...@standardnotes/models@1.10.3) (2022-05-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.10.2](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.10.1...@standardnotes/models@1.10.2) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.10.0...@standardnotes/models@1.10.1) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.9.0...@standardnotes/models@1.10.0) (2022-05-22)
|
||||
|
||||
### Features
|
||||
|
||||
* optional files navigation ([#745](https://github.com/standardnotes/snjs/issues/745)) ([8512166](https://github.com/standardnotes/snjs/commit/851216615478b57b11a570173f94ee598bec31c0))
|
||||
|
||||
# [1.9.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.8...@standardnotes/models@1.9.0) (2022-05-21)
|
||||
|
||||
### Features
|
||||
|
||||
* display controllers ([#743](https://github.com/standardnotes/snjs/issues/743)) ([5fadce3](https://github.com/standardnotes/snjs/commit/5fadce37f1b3f2f51b8a90c257bc666ac7710074))
|
||||
|
||||
## [1.8.8](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.7...@standardnotes/models@1.8.8) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.8.7](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.6...@standardnotes/models@1.8.7) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.8.6](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.5...@standardnotes/models@1.8.6) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.8.5](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.4...@standardnotes/models@1.8.5) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.8.4](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.3...@standardnotes/models@1.8.4) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.8.3](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.2...@standardnotes/models@1.8.3) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.8.2](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.1...@standardnotes/models@1.8.2) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.8.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.8.0...@standardnotes/models@1.8.1) (2022-05-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.8.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.7.1...@standardnotes/models@1.8.0) (2022-05-12)
|
||||
|
||||
### Features
|
||||
|
||||
* file desktop backups ([#731](https://github.com/standardnotes/snjs/issues/731)) ([0dbce7d](https://github.com/standardnotes/snjs/commit/0dbce7dc9712fde848445b951079c81479c8bc11))
|
||||
|
||||
## [1.7.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.7.0...@standardnotes/models@1.7.1) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.10...@standardnotes/models@1.7.0) (2022-05-12)
|
||||
|
||||
### Features
|
||||
|
||||
* new mobile-specific pref keys ([#730](https://github.com/standardnotes/snjs/issues/730)) ([cbf86a3](https://github.com/standardnotes/snjs/commit/cbf86a310e48a238ec8d8a5fd3d5c79da9120bd3))
|
||||
|
||||
## [1.6.10](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.9...@standardnotes/models@1.6.10) (2022-05-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* no conflict on files ([#728](https://github.com/standardnotes/snjs/issues/728)) ([9d1273d](https://github.com/standardnotes/snjs/commit/9d1273d21b299be826ff996fc97381242c13e8f1))
|
||||
|
||||
## [1.6.9](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.8...@standardnotes/models@1.6.9) (2022-05-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.6.8](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.7...@standardnotes/models@1.6.8) (2022-05-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update note count after remote delete ([#725](https://github.com/standardnotes/snjs/issues/725)) ([043edce](https://github.com/standardnotes/snjs/commit/043edcea9dfc7a8b234363910791f173880efdb9))
|
||||
|
||||
## [1.6.7](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.6...@standardnotes/models@1.6.7) (2022-05-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.6.6](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.5...@standardnotes/models@1.6.6) (2022-05-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.6.5](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.3...@standardnotes/models@1.6.5) (2022-05-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.6.4](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.3...@standardnotes/models@1.6.4) (2022-05-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.6.3](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.2...@standardnotes/models@1.6.3) (2022-05-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.6.2](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.1...@standardnotes/models@1.6.2) (2022-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.6.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.6.0...@standardnotes/models@1.6.1) (2022-04-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.5.1...@standardnotes/models@1.6.0) (2022-04-27)
|
||||
|
||||
### Features
|
||||
|
||||
* make files sortable using setDisplayOptions ([#713](https://github.com/standardnotes/snjs/issues/713)) ([b2088bf](https://github.com/standardnotes/snjs/commit/b2088bfa169ddea9aeddf9dfb20a098991aed875))
|
||||
|
||||
## [1.5.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.5.0...@standardnotes/models@1.5.1) (2022-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.9...@standardnotes/models@1.5.0) (2022-04-27)
|
||||
|
||||
### Features
|
||||
|
||||
* file upload and download progress ([#711](https://github.com/standardnotes/snjs/issues/711)) ([79fceed](https://github.com/standardnotes/snjs/commit/79fceeda4066dc66142f18c9c7b110757ca67e69))
|
||||
|
||||
## [1.4.9](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.8...@standardnotes/models@1.4.9) (2022-04-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.4.8](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.7...@standardnotes/models@1.4.8) (2022-04-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.4.7](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.6...@standardnotes/models@1.4.7) (2022-04-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* abort key recovery after aborted challenge ([#703](https://github.com/standardnotes/snjs/issues/703)) ([a67fb7e](https://github.com/standardnotes/snjs/commit/a67fb7e8cde41a5c9fadf545933e35d525faeaf0))
|
||||
|
||||
## [1.4.6](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.5...@standardnotes/models@1.4.6) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.4.5](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.4...@standardnotes/models@1.4.5) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.4.4](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.3...@standardnotes/models@1.4.4) (2022-04-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* better conflict errored items ([#699](https://github.com/standardnotes/snjs/issues/699)) ([1feaddd](https://github.com/standardnotes/snjs/commit/1feadddb79a4b39d08b6de979a380984fec6c689))
|
||||
|
||||
## [1.4.3](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.2...@standardnotes/models@1.4.3) (2022-04-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly handle encrypted item changes in collections ([#698](https://github.com/standardnotes/snjs/issues/698)) ([8b23c65](https://github.com/standardnotes/snjs/commit/8b23c6555decbdc5099fc4228ff889f7e5c8eb85))
|
||||
|
||||
## [1.4.2](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.1...@standardnotes/models@1.4.2) (2022-04-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.4.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.4.0...@standardnotes/models@1.4.1) (2022-04-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make timestamps required in payload construction ([#695](https://github.com/standardnotes/snjs/issues/695)) ([b3326c0](https://github.com/standardnotes/snjs/commit/b3326c0a998cd9d44a76afc377f182885ef48275))
|
||||
|
||||
# [1.4.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.3.0...@standardnotes/models@1.4.0) (2022-04-15)
|
||||
|
||||
### Features
|
||||
|
||||
* introduce sync resolved payloads to ensure deltas always return up to date dirty state ([#694](https://github.com/standardnotes/snjs/issues/694)) ([e5278ba](https://github.com/standardnotes/snjs/commit/e5278ba0b2afa987c37f009a2101fb91949d44c6))
|
||||
|
||||
# [1.3.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.2.6...@standardnotes/models@1.3.0) (2022-04-15)
|
||||
|
||||
### Features
|
||||
|
||||
* no merge payloads in payload manager ([#693](https://github.com/standardnotes/snjs/issues/693)) ([68a577c](https://github.com/standardnotes/snjs/commit/68a577cb887fd2d5556dc9ddec461f6ae665fcb6))
|
||||
|
||||
## [1.2.6](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.2.5...@standardnotes/models@1.2.6) (2022-04-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.2.5](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.2.4...@standardnotes/models@1.2.5) (2022-04-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* map ignored item timestamps so application remains in sync ([#692](https://github.com/standardnotes/snjs/issues/692)) ([966cbb0](https://github.com/standardnotes/snjs/commit/966cbb0c254d4d95c802bd8951488a499d1f8bef))
|
||||
|
||||
## [1.2.4](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.2.3...@standardnotes/models@1.2.4) (2022-04-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* emit changed deleted items as removed ([#691](https://github.com/standardnotes/snjs/issues/691)) ([b12f257](https://github.com/standardnotes/snjs/commit/b12f257b02d46ad9c717e6c51d6e7ca7e9c06959))
|
||||
|
||||
## [1.2.3](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.2.2...@standardnotes/models@1.2.3) (2022-04-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.2.2](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.2.1...@standardnotes/models@1.2.2) (2022-04-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.2.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.2.0...@standardnotes/models@1.2.1) (2022-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.2.0](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.1.2...@standardnotes/models@1.2.0) (2022-04-01)
|
||||
|
||||
### Features
|
||||
|
||||
* content interfaces and model type strictness ([#685](https://github.com/standardnotes/snjs/issues/685)) ([e2450c5](https://github.com/standardnotes/snjs/commit/e2450c59e8309d7080efaa03905b2abc728d9403))
|
||||
|
||||
## [1.1.2](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.1.1...@standardnotes/models@1.1.2) (2022-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.1.1](https://github.com/standardnotes/snjs/compare/@standardnotes/models@1.1.0...@standardnotes/models@1.1.1) (2022-03-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# 1.1.0 (2022-03-31)
|
||||
|
||||
### Features
|
||||
|
||||
* encryption and models packages ([#679](https://github.com/standardnotes/snjs/issues/679)) ([5e03d48](https://github.com/standardnotes/snjs/commit/5e03d48aba7e3dd266117201139ab869b1f70cc9))
|
||||
11
packages/models/jest.config.js
Normal file
11
packages/models/jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../node_modules/@standardnotes/config/src/jest.json');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
},
|
||||
}
|
||||
};
|
||||
4
packages/models/linter.tsconfig.json
Normal file
4
packages/models/linter.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
43
packages/models/package.json
Normal file
43
packages/models/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.13.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "Models used in SNJS library",
|
||||
"main": "dist/index.js",
|
||||
"author": "Standard Notes",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prestart": "yarn clean",
|
||||
"start": "tsc -p tsconfig.json --watch",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test:unit": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^27.5.1",
|
||||
"ts-jest": "^27.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.23.1",
|
||||
"@standardnotes/features": "workspace:*",
|
||||
"@standardnotes/responses": "^1.6.39",
|
||||
"@standardnotes/utils": "^1.6.12",
|
||||
"lodash": "^4.17.21",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
}
|
||||
}
|
||||
51
packages/models/src/Domain/Abstract/Content/ItemContent.ts
Normal file
51
packages/models/src/Domain/Abstract/Content/ItemContent.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { AppData, DefaultAppDomain } from '../Item/Types/DefaultAppDomain'
|
||||
import { ContentReference } from '../Reference/ContentReference'
|
||||
import { AppDataField } from '../Item/Types/AppDataField'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SpecializedContent {}
|
||||
|
||||
export interface ItemContent {
|
||||
references: ContentReference[]
|
||||
conflict_of?: Uuid
|
||||
protected?: boolean
|
||||
trashed?: boolean
|
||||
pinned?: boolean
|
||||
archived?: boolean
|
||||
locked?: boolean
|
||||
appData?: AppData
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the input object to fill in any missing required values from the
|
||||
* content body.
|
||||
*/
|
||||
|
||||
export function FillItemContent<C extends ItemContent = ItemContent>(content: Partial<C>): C {
|
||||
if (!content.references) {
|
||||
content.references = []
|
||||
}
|
||||
|
||||
if (!content.appData) {
|
||||
content.appData = {
|
||||
[DefaultAppDomain]: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!content.appData[DefaultAppDomain]) {
|
||||
content.appData[DefaultAppDomain] = {}
|
||||
}
|
||||
|
||||
if (!content.appData[DefaultAppDomain][AppDataField.UserModifiedDate]) {
|
||||
content.appData[DefaultAppDomain][AppDataField.UserModifiedDate] = `${new Date()}`
|
||||
}
|
||||
|
||||
return content as C
|
||||
}
|
||||
|
||||
export function FillItemContentSpecialized<S extends SpecializedContent, C extends ItemContent = ItemContent>(
|
||||
content: S,
|
||||
): C {
|
||||
return FillItemContent(content)
|
||||
}
|
||||
60
packages/models/src/Domain/Abstract/Contextual/BackupFile.ts
Normal file
60
packages/models/src/Domain/Abstract/Contextual/BackupFile.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { ContextPayload } from './ContextPayload'
|
||||
import { ItemContent } from '../Content/ItemContent'
|
||||
import { DecryptedTransferPayload, EncryptedTransferPayload } from '../TransferPayload'
|
||||
|
||||
export interface BackupFileEncryptedContextualPayload extends ContextPayload {
|
||||
auth_hash?: string
|
||||
content: string
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
duplicate_of?: Uuid
|
||||
enc_item_key: string
|
||||
items_key_id: string | undefined
|
||||
updated_at: Date
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
|
||||
export interface BackupFileDecryptedContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
|
||||
content: C
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
duplicate_of?: Uuid
|
||||
updated_at: Date
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
|
||||
export function CreateEncryptedBackupFileContextPayload(
|
||||
fromPayload: EncryptedTransferPayload,
|
||||
): BackupFileEncryptedContextualPayload {
|
||||
return {
|
||||
auth_hash: fromPayload.auth_hash,
|
||||
content_type: fromPayload.content_type,
|
||||
content: fromPayload.content,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
enc_item_key: fromPayload.enc_item_key,
|
||||
items_key_id: fromPayload.items_key_id,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateDecryptedBackupFileContextPayload(
|
||||
fromPayload: DecryptedTransferPayload,
|
||||
): BackupFileDecryptedContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
content: fromPayload.content,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ItemContent } from '../Content/ItemContent'
|
||||
import { DecryptedTransferPayload } from '../TransferPayload'
|
||||
import { ContextPayload } from './ContextPayload'
|
||||
|
||||
/**
|
||||
* Represents a payload with permissible fields for when a
|
||||
* component wants to create a new item
|
||||
*/
|
||||
export interface ComponentCreateContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
|
||||
content: C
|
||||
created_at?: Date
|
||||
}
|
||||
|
||||
export function createComponentCreatedContextPayload(
|
||||
fromPayload: DecryptedTransferPayload,
|
||||
): ComponentCreateContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
content: fromPayload.content,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
uuid: fromPayload.uuid,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ItemContent } from '../Content/ItemContent'
|
||||
import { DecryptedTransferPayload } from '../TransferPayload'
|
||||
import { ContextPayload } from './ContextPayload'
|
||||
|
||||
/**
|
||||
* Represents a payload with permissible fields for when a
|
||||
* payload is retrieved from a component for saving
|
||||
*/
|
||||
export interface ComponentRetrievedContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
|
||||
content: C
|
||||
created_at?: Date
|
||||
}
|
||||
|
||||
export function CreateComponentRetrievedContextPayload(
|
||||
fromPayload: DecryptedTransferPayload,
|
||||
): ComponentRetrievedContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
content: fromPayload.content,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
uuid: fromPayload.uuid,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ItemContent } from '../Content/ItemContent'
|
||||
|
||||
export interface ContextPayload<C extends ItemContent = ItemContent> {
|
||||
uuid: string
|
||||
content_type: ContentType
|
||||
content: C | string | undefined
|
||||
deleted: boolean
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ServerItemResponse } from '@standardnotes/responses'
|
||||
import { isCorruptTransferPayload, isEncryptedTransferPayload } from '../TransferPayload'
|
||||
|
||||
export interface FilteredServerItem extends ServerItemResponse {
|
||||
__passed_filter__: true
|
||||
}
|
||||
|
||||
export function CreateFilteredServerItem(item: ServerItemResponse): FilteredServerItem {
|
||||
return {
|
||||
...item,
|
||||
__passed_filter__: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function FilterDisallowedRemotePayloadsAndMap(payloads: ServerItemResponse[]): FilteredServerItem[] {
|
||||
return payloads.filter(isRemotePayloadAllowed).map(CreateFilteredServerItem)
|
||||
}
|
||||
|
||||
export function isRemotePayloadAllowed(payload: ServerItemResponse): boolean {
|
||||
if (isCorruptTransferPayload(payload)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isEncryptedTransferPayload(payload) || payload.content == undefined
|
||||
}
|
||||
107
packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts
Normal file
107
packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { ContextPayload } from './ContextPayload'
|
||||
import { ItemContent } from '../Content/ItemContent'
|
||||
import { DecryptedPayloadInterface, DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload'
|
||||
import { useBoolean } from '@standardnotes/utils'
|
||||
import { EncryptedTransferPayload, isEncryptedTransferPayload } from '../TransferPayload'
|
||||
|
||||
export function isEncryptedLocalStoragePayload(
|
||||
p: LocalStorageEncryptedContextualPayload | LocalStorageDecryptedContextualPayload,
|
||||
): p is LocalStorageEncryptedContextualPayload {
|
||||
return isEncryptedTransferPayload(p as EncryptedTransferPayload)
|
||||
}
|
||||
|
||||
export interface LocalStorageEncryptedContextualPayload extends ContextPayload {
|
||||
auth_hash?: string
|
||||
auth_params?: unknown
|
||||
content: string
|
||||
deleted: false
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
dirty: boolean
|
||||
duplicate_of: Uuid | undefined
|
||||
enc_item_key: string
|
||||
errorDecrypting: boolean
|
||||
items_key_id: string | undefined
|
||||
updated_at_timestamp: number
|
||||
updated_at: Date
|
||||
waitingForKey: boolean
|
||||
}
|
||||
|
||||
export interface LocalStorageDecryptedContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
|
||||
content: C
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
deleted: false
|
||||
dirty: boolean
|
||||
duplicate_of?: Uuid
|
||||
updated_at_timestamp: number
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
export interface LocalStorageDeletedContextualPayload extends ContextPayload {
|
||||
content: undefined
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
deleted: true
|
||||
dirty: true
|
||||
duplicate_of?: Uuid
|
||||
updated_at_timestamp: number
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
export function CreateEncryptedLocalStorageContextPayload(
|
||||
fromPayload: EncryptedPayloadInterface,
|
||||
): LocalStorageEncryptedContextualPayload {
|
||||
return {
|
||||
auth_hash: fromPayload.auth_hash,
|
||||
content_type: fromPayload.content_type,
|
||||
content: fromPayload.content,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
dirty: fromPayload.dirty != undefined ? fromPayload.dirty : false,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
enc_item_key: fromPayload.enc_item_key,
|
||||
errorDecrypting: fromPayload.errorDecrypting,
|
||||
items_key_id: fromPayload.items_key_id,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
waitingForKey: fromPayload.waitingForKey,
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateDecryptedLocalStorageContextPayload(
|
||||
fromPayload: DecryptedPayloadInterface,
|
||||
): LocalStorageDecryptedContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
content: fromPayload.content,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
dirty: useBoolean(fromPayload.dirty, false),
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateDeletedLocalStorageContextPayload(
|
||||
fromPayload: DeletedPayloadInterface,
|
||||
): LocalStorageDeletedContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
content: undefined,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: true,
|
||||
dirty: true,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { ItemContent } from '../Content/ItemContent'
|
||||
import { DecryptedPayloadInterface, DeletedPayloadInterface, isDecryptedPayload } from '../Payload'
|
||||
import { ContextPayload } from './ContextPayload'
|
||||
|
||||
export interface OfflineSyncPushContextualPayload extends ContextPayload {
|
||||
content: ItemContent | undefined
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
duplicate_of?: Uuid
|
||||
updated_at_timestamp: number
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
export function CreateOfflineSyncPushContextPayload(
|
||||
fromPayload: DecryptedPayloadInterface | DeletedPayloadInterface,
|
||||
): OfflineSyncPushContextualPayload {
|
||||
const base: OfflineSyncPushContextualPayload = {
|
||||
content: undefined,
|
||||
content_type: fromPayload.content_type,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
}
|
||||
|
||||
if (isDecryptedPayload(fromPayload)) {
|
||||
return {
|
||||
...base,
|
||||
content: fromPayload.content,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...base,
|
||||
deleted: fromPayload.deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedPayloadInterface, DeletedPayloadInterface, isDeletedPayload } from '../Payload'
|
||||
|
||||
/**
|
||||
* The saved sync item payload represents the payload we want to map
|
||||
* when mapping saved_items from the server or local sync mechanism. We only want to map the
|
||||
* updated_at value the server returns for the item, and basically
|
||||
* nothing else.
|
||||
*/
|
||||
export interface OfflineSyncSavedContextualPayload {
|
||||
content_type: ContentType
|
||||
created_at_timestamp: number
|
||||
deleted: boolean
|
||||
updated_at_timestamp?: number
|
||||
updated_at: Date
|
||||
uuid: string
|
||||
}
|
||||
|
||||
export function CreateOfflineSyncSavedPayload(
|
||||
fromPayload: DecryptedPayloadInterface | DeletedPayloadInterface,
|
||||
): OfflineSyncSavedContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
deleted: isDeletedPayload(fromPayload),
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload'
|
||||
import { ContextPayload } from './ContextPayload'
|
||||
|
||||
export interface ServerSyncPushContextualPayload extends ContextPayload {
|
||||
auth_hash?: string
|
||||
content: string | undefined
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
duplicate_of?: Uuid
|
||||
enc_item_key?: string
|
||||
items_key_id?: string
|
||||
updated_at_timestamp: number
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
export function CreateEncryptedServerSyncPushPayload(
|
||||
fromPayload: EncryptedPayloadInterface,
|
||||
): ServerSyncPushContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: false,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
content: fromPayload.content,
|
||||
enc_item_key: fromPayload.enc_item_key,
|
||||
items_key_id: fromPayload.items_key_id,
|
||||
auth_hash: fromPayload.auth_hash,
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateDeletedServerSyncPushPayload(
|
||||
fromPayload: DeletedPayloadInterface,
|
||||
): ServerSyncPushContextualPayload {
|
||||
return {
|
||||
content_type: fromPayload.content_type,
|
||||
created_at_timestamp: fromPayload.created_at_timestamp,
|
||||
created_at: fromPayload.created_at,
|
||||
deleted: true,
|
||||
duplicate_of: fromPayload.duplicate_of,
|
||||
updated_at_timestamp: fromPayload.updated_at_timestamp,
|
||||
updated_at: fromPayload.updated_at,
|
||||
uuid: fromPayload.uuid,
|
||||
content: undefined,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useBoolean } from '@standardnotes/utils'
|
||||
import { FilteredServerItem } from './FilteredServerItem'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
/**
|
||||
* The saved sync item payload represents the payload we want to map
|
||||
* when mapping saved_items from the server. We only want to map the
|
||||
* updated_at value the server returns for the item, and basically
|
||||
* nothing else.
|
||||
*/
|
||||
export interface ServerSyncSavedContextualPayload {
|
||||
content_type: ContentType
|
||||
created_at_timestamp: number
|
||||
created_at: Date
|
||||
deleted: boolean
|
||||
updated_at_timestamp: number
|
||||
updated_at: Date
|
||||
uuid: string
|
||||
}
|
||||
|
||||
export function CreateServerSyncSavedPayload(from: FilteredServerItem): ServerSyncSavedContextualPayload {
|
||||
return {
|
||||
content_type: from.content_type,
|
||||
created_at_timestamp: from.created_at_timestamp,
|
||||
created_at: from.created_at,
|
||||
deleted: useBoolean(from.deleted, false),
|
||||
updated_at_timestamp: from.updated_at_timestamp,
|
||||
updated_at: from.updated_at,
|
||||
uuid: from.uuid,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ItemContent } from '../Content/ItemContent'
|
||||
import { ContextPayload } from './ContextPayload'
|
||||
|
||||
export interface SessionHistoryContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
|
||||
content: C
|
||||
updated_at: Date
|
||||
}
|
||||
10
packages/models/src/Domain/Abstract/Contextual/index.ts
Normal file
10
packages/models/src/Domain/Abstract/Contextual/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './ComponentCreate'
|
||||
export * from './ComponentRetrieved'
|
||||
export * from './BackupFile'
|
||||
export * from './LocalStorage'
|
||||
export * from './OfflineSyncPush'
|
||||
export * from './OfflineSyncSaved'
|
||||
export * from './ServerSyncPush'
|
||||
export * from './SessionHistory'
|
||||
export * from './ServerSyncSaved'
|
||||
export * from './FilteredServerItem'
|
||||
@@ -0,0 +1,122 @@
|
||||
import { dateToLocalizedString, useBoolean } from '@standardnotes/utils'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { DecryptedTransferPayload } from './../../TransferPayload/Interfaces/DecryptedTransferPayload'
|
||||
import { AppDataField } from '../Types/AppDataField'
|
||||
import { ComponentDataDomain, DefaultAppDomain } from '../Types/DefaultAppDomain'
|
||||
import { DecryptedItemInterface } from '../Interfaces/DecryptedItem'
|
||||
import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload'
|
||||
import { GenericItem } from './GenericItem'
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { ItemContentsEqual } from '../../../Utilities/Item/ItemContentsEqual'
|
||||
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
|
||||
import { ContentReference } from '../../Reference/ContentReference'
|
||||
|
||||
export class DecryptedItem<C extends ItemContent = ItemContent>
|
||||
extends GenericItem<DecryptedPayloadInterface<C>>
|
||||
implements DecryptedItemInterface<C>
|
||||
{
|
||||
public readonly conflictOf?: Uuid
|
||||
public readonly protected: boolean = false
|
||||
public readonly trashed: boolean = false
|
||||
public readonly pinned: boolean = false
|
||||
public readonly archived: boolean = false
|
||||
public readonly locked: boolean = false
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<C>) {
|
||||
super(payload)
|
||||
this.conflictOf = payload.content.conflict_of
|
||||
|
||||
const userModVal = this.getAppDomainValueWithDefault(AppDataField.UserModifiedDate, this.serverUpdatedAt || 0)
|
||||
|
||||
this.userModifiedDate = new Date(userModVal as number | Date)
|
||||
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
|
||||
this.protected = useBoolean(this.payload.content.protected, false)
|
||||
this.trashed = useBoolean(this.payload.content.trashed, false)
|
||||
this.pinned = this.getAppDomainValueWithDefault(AppDataField.Pinned, false)
|
||||
this.archived = this.getAppDomainValueWithDefault(AppDataField.Archived, false)
|
||||
this.locked = this.getAppDomainValueWithDefault(AppDataField.Locked, false)
|
||||
}
|
||||
|
||||
public static DefaultAppDomain() {
|
||||
return DefaultAppDomain
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.payload.content
|
||||
}
|
||||
|
||||
get references(): ContentReference[] {
|
||||
return this.payload.content.references || []
|
||||
}
|
||||
|
||||
public isReferencingItem(item: DecryptedItemInterface): boolean {
|
||||
return this.references.find((r) => r.uuid === item.uuid) != undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Inside of content is a record called `appData` (which should have been called `domainData`).
|
||||
* It was named `appData` as a way to indicate that it can house data for multiple apps.
|
||||
* Each key of appData is a domain string, which was originally designed
|
||||
* to allow for multiple 3rd party apps who share access to the same data to store data
|
||||
* in an isolated location. This design premise is antiquited and no longer pursued,
|
||||
* however we continue to use it as not to uncesesarily create a large data migration
|
||||
* that would require users to sync all their data.
|
||||
*
|
||||
* domainData[DomainKey] will give you another Record<string, any>.
|
||||
*
|
||||
* Currently appData['org.standardnotes.sn'] returns an object of type AppData.
|
||||
* And appData['org.standardnotes.sn.components] returns an object of type ComponentData
|
||||
*/
|
||||
public getDomainData(
|
||||
domain: typeof ComponentDataDomain | typeof DefaultAppDomain,
|
||||
): undefined | Record<string, unknown> {
|
||||
const domainData = this.payload.content.appData
|
||||
if (!domainData) {
|
||||
return undefined
|
||||
}
|
||||
const data = domainData[domain]
|
||||
return data
|
||||
}
|
||||
|
||||
public getAppDomainValue<T>(key: AppDataField | PrefKey): T | undefined {
|
||||
const appData = this.getDomainData(DefaultAppDomain)
|
||||
return appData?.[key] as T
|
||||
}
|
||||
|
||||
public getAppDomainValueWithDefault<T, D extends T>(key: AppDataField | PrefKey, defaultValue: D): T {
|
||||
const appData = this.getDomainData(DefaultAppDomain)
|
||||
return (appData?.[key] as T) || defaultValue
|
||||
}
|
||||
|
||||
public override payloadRepresentation(override?: Partial<DecryptedTransferPayload<C>>): DecryptedPayloadInterface<C> {
|
||||
return this.payload.copy(override)
|
||||
}
|
||||
|
||||
/**
|
||||
* During sync conflicts, when determing whether to create a duplicate for an item,
|
||||
* we can omit keys that have no meaningful weight and can be ignored. For example,
|
||||
* if one component has active = true and another component has active = false,
|
||||
* it would be needless to duplicate them, so instead we ignore that value.
|
||||
*/
|
||||
public contentKeysToIgnoreWhenCheckingEquality<C extends ItemContent = ItemContent>(): (keyof C)[] {
|
||||
return ['conflict_of']
|
||||
}
|
||||
|
||||
/** Same as `contentKeysToIgnoreWhenCheckingEquality`, but keys inside appData[Item.AppDomain] */
|
||||
public appDataContentKeysToIgnoreWhenCheckingEquality(): AppDataField[] {
|
||||
return [AppDataField.UserModifiedDate]
|
||||
}
|
||||
|
||||
public getContentCopy() {
|
||||
return JSON.parse(JSON.stringify(this.content))
|
||||
}
|
||||
|
||||
public isItemContentEqualWith(otherItem: DecryptedItemInterface) {
|
||||
return ItemContentsEqual(
|
||||
this.payload.content,
|
||||
otherItem.payload.content,
|
||||
this.contentKeysToIgnoreWhenCheckingEquality(),
|
||||
this.appDataContentKeysToIgnoreWhenCheckingEquality(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { GenericItem } from './GenericItem'
|
||||
import { DeletedPayloadInterface } from '../../Payload'
|
||||
import { DeletedItemInterface } from '../Interfaces/DeletedItem'
|
||||
import { DeletedTransferPayload } from '../../TransferPayload'
|
||||
|
||||
export class DeletedItem extends GenericItem<DeletedPayloadInterface> implements DeletedItemInterface {
|
||||
deleted: true
|
||||
content: undefined
|
||||
|
||||
constructor(payload: DeletedPayloadInterface) {
|
||||
super(payload)
|
||||
this.deleted = true
|
||||
}
|
||||
|
||||
public override payloadRepresentation(override?: Partial<DeletedTransferPayload>): DeletedPayloadInterface {
|
||||
return this.payload.copy(override)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { EncryptedTransferPayload } from './../../TransferPayload/Interfaces/EncryptedTransferPayload'
|
||||
import { EncryptedItemInterface } from '../Interfaces/EncryptedItem'
|
||||
import { EncryptedPayloadInterface } from '../../Payload/Interfaces/EncryptedPayload'
|
||||
import { GenericItem } from './GenericItem'
|
||||
|
||||
export class EncryptedItem extends GenericItem<EncryptedPayloadInterface> implements EncryptedItemInterface {
|
||||
constructor(payload: EncryptedPayloadInterface) {
|
||||
super(payload)
|
||||
}
|
||||
|
||||
get version() {
|
||||
return this.payload.version
|
||||
}
|
||||
|
||||
public override payloadRepresentation(override?: Partial<EncryptedTransferPayload>): EncryptedPayloadInterface {
|
||||
return this.payload.copy(override)
|
||||
}
|
||||
|
||||
get errorDecrypting() {
|
||||
return this.payload.errorDecrypting
|
||||
}
|
||||
|
||||
get waitingForKey() {
|
||||
return this.payload.waitingForKey
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.payload.content
|
||||
}
|
||||
|
||||
get auth_hash() {
|
||||
return this.payload.auth_hash
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { dateToLocalizedString, deepFreeze } from '@standardnotes/utils'
|
||||
import { TransferPayload } from './../../TransferPayload/Interfaces/TransferPayload'
|
||||
import { ItemContentsDiffer } from '../../../Utilities/Item/ItemContentsDiffer'
|
||||
import { ItemInterface } from '../Interfaces/ItemInterface'
|
||||
import { PayloadSource } from '../../Payload/Types/PayloadSource'
|
||||
import { ConflictStrategy } from '../Types/ConflictStrategy'
|
||||
import { PredicateInterface } from '../../../Runtime/Predicate/Interface'
|
||||
import { SingletonStrategy } from '../Types/SingletonStrategy'
|
||||
import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface'
|
||||
import { HistoryEntryInterface } from '../../../Runtime/History/HistoryEntryInterface'
|
||||
import { isDecryptedItem, isDeletedItem, isEncryptedErroredItem } from '../Interfaces/TypeCheck'
|
||||
|
||||
export abstract class GenericItem<P extends PayloadInterface = PayloadInterface> implements ItemInterface<P> {
|
||||
payload: P
|
||||
public readonly duplicateOf?: Uuid
|
||||
public readonly createdAtString?: string
|
||||
public updatedAtString?: string
|
||||
public userModifiedDate: Date
|
||||
|
||||
constructor(payload: P) {
|
||||
this.payload = payload
|
||||
this.duplicateOf = payload.duplicate_of
|
||||
this.createdAtString = this.created_at && dateToLocalizedString(this.created_at)
|
||||
this.userModifiedDate = this.serverUpdatedAt || new Date()
|
||||
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
|
||||
|
||||
const timeToAllowSubclassesToFinishConstruction = 0
|
||||
setTimeout(() => {
|
||||
deepFreeze(this)
|
||||
}, timeToAllowSubclassesToFinishConstruction)
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this.payload.uuid
|
||||
}
|
||||
|
||||
get content_type(): ContentType {
|
||||
return this.payload.content_type
|
||||
}
|
||||
|
||||
get created_at() {
|
||||
return this.payload.created_at
|
||||
}
|
||||
|
||||
/**
|
||||
* The date timestamp the server set for this item upon it being synced
|
||||
* Undefined if never synced to a remote server.
|
||||
*/
|
||||
public get serverUpdatedAt(): Date {
|
||||
return this.payload.serverUpdatedAt
|
||||
}
|
||||
|
||||
public get serverUpdatedAtTimestamp(): number | undefined {
|
||||
return this.payload.updated_at_timestamp
|
||||
}
|
||||
|
||||
/** @deprecated Use serverUpdatedAt instead */
|
||||
public get updated_at(): Date | undefined {
|
||||
return this.serverUpdatedAt
|
||||
}
|
||||
|
||||
get dirty() {
|
||||
return this.payload.dirty
|
||||
}
|
||||
|
||||
get lastSyncBegan() {
|
||||
return this.payload.lastSyncBegan
|
||||
}
|
||||
|
||||
get lastSyncEnd() {
|
||||
return this.payload.lastSyncEnd
|
||||
}
|
||||
|
||||
get duplicate_of() {
|
||||
return this.payload.duplicate_of
|
||||
}
|
||||
|
||||
public payloadRepresentation(override?: Partial<TransferPayload>): P {
|
||||
return this.payload.copy(override)
|
||||
}
|
||||
|
||||
/** Whether the item has never been synced to a server */
|
||||
public get neverSynced(): boolean {
|
||||
return !this.serverUpdatedAt || this.serverUpdatedAt.getTime() === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can override this getter to return true if they want only
|
||||
* one of this item to exist, depending on custom criteria.
|
||||
*/
|
||||
public get isSingleton(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/** The predicate by which singleton items should be unique */
|
||||
public singletonPredicate<T extends ItemInterface>(): PredicateInterface<T> {
|
||||
throw 'Must override SNItem.singletonPredicate'
|
||||
}
|
||||
|
||||
public get singletonStrategy(): SingletonStrategy {
|
||||
return SingletonStrategy.KeepEarliest
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can override this method and provide their own opinion on whether
|
||||
* they want to be duplicated. For example, if this.content.x = 12 and
|
||||
* item.content.x = 13, this function can be overriden to always return
|
||||
* ConflictStrategy.KeepBase to say 'don't create a duplicate at all, the
|
||||
* change is not important.'
|
||||
*
|
||||
* In the default implementation, we create a duplicate if content differs.
|
||||
* However, if they only differ by references, we KEEP_LEFT_MERGE_REFS.
|
||||
*
|
||||
* Left returns to our current item, and Right refers to the incoming item.
|
||||
*/
|
||||
public strategyWhenConflictingWithItem(
|
||||
item: ItemInterface,
|
||||
previousRevision?: HistoryEntryInterface,
|
||||
): ConflictStrategy {
|
||||
if (isEncryptedErroredItem(this)) {
|
||||
return ConflictStrategy.KeepBaseDuplicateApply
|
||||
}
|
||||
|
||||
if (this.isSingleton) {
|
||||
return ConflictStrategy.KeepBase
|
||||
}
|
||||
|
||||
if (isDeletedItem(this)) {
|
||||
return ConflictStrategy.KeepApply
|
||||
}
|
||||
|
||||
if (isDeletedItem(item)) {
|
||||
if (this.payload.source === PayloadSource.FileImport) {
|
||||
return ConflictStrategy.KeepBase
|
||||
}
|
||||
return ConflictStrategy.KeepApply
|
||||
}
|
||||
|
||||
if (!isDecryptedItem(item) || !isDecryptedItem(this)) {
|
||||
return ConflictStrategy.KeepBaseDuplicateApply
|
||||
}
|
||||
|
||||
const contentDiffers = ItemContentsDiffer(this, item)
|
||||
if (!contentDiffers) {
|
||||
return ConflictStrategy.KeepApply
|
||||
}
|
||||
|
||||
const itemsAreDifferentExcludingRefs = ItemContentsDiffer(this, item, ['references'])
|
||||
if (itemsAreDifferentExcludingRefs) {
|
||||
if (previousRevision) {
|
||||
/**
|
||||
* If previousRevision.content === incomingValue.content, this means the
|
||||
* change that was rejected by the server is in fact a legitimate change,
|
||||
* because the value the client had previously matched with the server's,
|
||||
* and this new change is being built on top of that state, and should therefore
|
||||
* be chosen as the winner, with no need for a conflict.
|
||||
*/
|
||||
if (!ItemContentsDiffer(previousRevision.itemFromPayload(), item)) {
|
||||
return ConflictStrategy.KeepBase
|
||||
}
|
||||
}
|
||||
const twentySeconds = 20_000
|
||||
if (
|
||||
/**
|
||||
* If the incoming item comes from an import, treat it as
|
||||
* less important than the existing one.
|
||||
*/
|
||||
item.payload.source === PayloadSource.FileImport ||
|
||||
/**
|
||||
* If the user is actively editing our item, duplicate the incoming item
|
||||
* to avoid creating surprises in the client's UI.
|
||||
*/
|
||||
Date.now() - this.userModifiedDate.getTime() < twentySeconds
|
||||
) {
|
||||
return ConflictStrategy.KeepBaseDuplicateApply
|
||||
} else {
|
||||
return ConflictStrategy.DuplicateBaseKeepApply
|
||||
}
|
||||
} else {
|
||||
/** Only the references have changed; merge them. */
|
||||
return ConflictStrategy.KeepBaseMergeRefs
|
||||
}
|
||||
}
|
||||
|
||||
public satisfiesPredicate(predicate: PredicateInterface<ItemInterface>): boolean {
|
||||
return predicate.matchesItem(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { AppDataField } from '../Types/AppDataField'
|
||||
import { ComponentDataDomain, DefaultAppDomain } from '../Types/DefaultAppDomain'
|
||||
import { ContentReference } from '../../Reference/ContentReference'
|
||||
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload'
|
||||
import { ItemInterface } from './ItemInterface'
|
||||
import { SortableItem } from '../../../Runtime/Collection/CollectionSort'
|
||||
import { DecryptedTransferPayload } from '../../TransferPayload/Interfaces/DecryptedTransferPayload'
|
||||
import { SearchableItem } from '../../../Runtime/Display'
|
||||
|
||||
export interface DecryptedItemInterface<C extends ItemContent = ItemContent>
|
||||
extends ItemInterface<DecryptedPayloadInterface<C>>,
|
||||
SortableItem,
|
||||
SearchableItem {
|
||||
readonly content: C
|
||||
readonly conflictOf?: Uuid
|
||||
readonly duplicateOf?: Uuid
|
||||
readonly protected: boolean
|
||||
readonly trashed: boolean
|
||||
readonly pinned: boolean
|
||||
readonly archived: boolean
|
||||
readonly locked: boolean
|
||||
readonly userModifiedDate: Date
|
||||
readonly references: ContentReference[]
|
||||
|
||||
getAppDomainValueWithDefault<T, D extends T>(key: AppDataField | PrefKey, defaultValue: D): T
|
||||
|
||||
getAppDomainValue<T>(key: AppDataField | PrefKey): T | undefined
|
||||
|
||||
isItemContentEqualWith(otherItem: DecryptedItemInterface): boolean
|
||||
|
||||
payloadRepresentation(override?: Partial<DecryptedTransferPayload<C>>): DecryptedPayloadInterface<C>
|
||||
|
||||
isReferencingItem(item: DecryptedItemInterface): boolean
|
||||
|
||||
getDomainData(domain: typeof ComponentDataDomain | typeof DefaultAppDomain): undefined | Record<string, unknown>
|
||||
|
||||
contentKeysToIgnoreWhenCheckingEquality<C extends ItemContent = ItemContent>(): (keyof C)[]
|
||||
|
||||
appDataContentKeysToIgnoreWhenCheckingEquality(): AppDataField[]
|
||||
|
||||
getContentCopy(): C
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DeletedPayloadInterface } from './../../Payload/Interfaces/DeletedPayload'
|
||||
import { ItemInterface } from './ItemInterface'
|
||||
|
||||
export interface DeletedItemInterface extends ItemInterface<DeletedPayloadInterface> {
|
||||
readonly deleted: true
|
||||
readonly content: undefined
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { EncryptedPayloadInterface } from '../../Payload/Interfaces/EncryptedPayload'
|
||||
import { ItemInterface } from './ItemInterface'
|
||||
|
||||
export interface EncryptedItemInterface extends ItemInterface<EncryptedPayloadInterface> {
|
||||
content: string
|
||||
version: ProtocolVersion
|
||||
errorDecrypting: boolean
|
||||
waitingForKey?: boolean
|
||||
auth_hash?: string
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Uuid, ContentType } from '@standardnotes/common'
|
||||
import { TransferPayload } from './../../TransferPayload/Interfaces/TransferPayload'
|
||||
import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface'
|
||||
import { PredicateInterface } from '../../../Runtime/Predicate/Interface'
|
||||
import { HistoryEntryInterface } from '../../../Runtime/History'
|
||||
import { ConflictStrategy } from '../Types/ConflictStrategy'
|
||||
import { SingletonStrategy } from '../Types/SingletonStrategy'
|
||||
|
||||
export interface ItemInterface<P extends PayloadInterface = PayloadInterface> {
|
||||
payload: P
|
||||
readonly conflictOf?: Uuid
|
||||
readonly duplicateOf?: Uuid
|
||||
readonly createdAtString?: string
|
||||
readonly updatedAtString?: string
|
||||
|
||||
uuid: Uuid
|
||||
|
||||
content_type: ContentType
|
||||
created_at: Date
|
||||
serverUpdatedAt: Date
|
||||
serverUpdatedAtTimestamp: number | undefined
|
||||
dirty: boolean | undefined
|
||||
|
||||
lastSyncBegan: Date | undefined
|
||||
lastSyncEnd: Date | undefined
|
||||
neverSynced: boolean
|
||||
|
||||
duplicate_of: string | undefined
|
||||
isSingleton: boolean
|
||||
updated_at: Date | undefined
|
||||
|
||||
singletonPredicate<T extends ItemInterface>(): PredicateInterface<T>
|
||||
|
||||
singletonStrategy: SingletonStrategy
|
||||
|
||||
strategyWhenConflictingWithItem(item: ItemInterface, previousRevision?: HistoryEntryInterface): ConflictStrategy
|
||||
|
||||
satisfiesPredicate(predicate: PredicateInterface<ItemInterface>): boolean
|
||||
|
||||
payloadRepresentation(override?: Partial<TransferPayload>): P
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EncryptedItemInterface } from './EncryptedItem'
|
||||
import { DeletedItemInterface } from './DeletedItem'
|
||||
import { ItemInterface } from './ItemInterface'
|
||||
import { DecryptedItemInterface } from './DecryptedItem'
|
||||
import { isDecryptedPayload, isDeletedPayload, isEncryptedPayload } from '../../Payload/Interfaces/TypeCheck'
|
||||
|
||||
export function isDecryptedItem(item: ItemInterface): item is DecryptedItemInterface {
|
||||
return isDecryptedPayload(item.payload)
|
||||
}
|
||||
|
||||
export function isEncryptedItem(item: ItemInterface): item is EncryptedItemInterface {
|
||||
return isEncryptedPayload(item.payload)
|
||||
}
|
||||
|
||||
export function isNotEncryptedItem(
|
||||
item: DecryptedItemInterface | DeletedItemInterface | EncryptedItemInterface,
|
||||
): item is DecryptedItemInterface | DeletedItemInterface {
|
||||
return !isEncryptedItem(item)
|
||||
}
|
||||
|
||||
export function isDeletedItem(item: ItemInterface): item is DeletedItemInterface {
|
||||
return isDeletedPayload(item.payload)
|
||||
}
|
||||
|
||||
export function isDecryptedOrDeletedItem(item: ItemInterface): item is DecryptedItemInterface | DeletedItemInterface {
|
||||
return isDecryptedItem(item) || isDeletedItem(item)
|
||||
}
|
||||
|
||||
export function isEncryptedErroredItem(item: ItemInterface): boolean {
|
||||
return isEncryptedItem(item) && item.errorDecrypting === true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { DecryptedItemInterface } from './DecryptedItem'
|
||||
import { DeletedItemInterface } from './DeletedItem'
|
||||
import { EncryptedItemInterface } from './EncryptedItem'
|
||||
|
||||
export type AnyItemInterface<C extends ItemContent = ItemContent> =
|
||||
| EncryptedItemInterface
|
||||
| DecryptedItemInterface<C>
|
||||
| DeletedItemInterface
|
||||
@@ -0,0 +1,145 @@
|
||||
import { DecryptedItemInterface } from './../Interfaces/DecryptedItem'
|
||||
import { Copy } from '@standardnotes/utils'
|
||||
import { MutationType } from '../Types/MutationType'
|
||||
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { AppDataField } from '../Types/AppDataField'
|
||||
import { DefaultAppDomain, DomainDataValueType, ItemDomainKey } from '../Types/DefaultAppDomain'
|
||||
import { ItemMutator } from './ItemMutator'
|
||||
import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload'
|
||||
import { ItemInterface } from '../Interfaces/ItemInterface'
|
||||
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
|
||||
|
||||
export class DecryptedItemMutator<C extends ItemContent = ItemContent> extends ItemMutator<
|
||||
DecryptedPayloadInterface<C>,
|
||||
DecryptedItemInterface<C>
|
||||
> {
|
||||
protected mutableContent: C
|
||||
|
||||
constructor(item: DecryptedItemInterface<C>, type: MutationType) {
|
||||
super(item, type)
|
||||
|
||||
const mutableCopy = Copy(this.immutablePayload.content)
|
||||
this.mutableContent = mutableCopy
|
||||
}
|
||||
|
||||
public override getResult() {
|
||||
if (this.type === MutationType.NonDirtying) {
|
||||
return this.immutablePayload.copy({
|
||||
content: this.mutableContent,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.type === MutationType.UpdateUserTimestamps) {
|
||||
this.userModifiedDate = new Date()
|
||||
} else {
|
||||
const currentValue = this.immutableItem.userModifiedDate
|
||||
if (!currentValue) {
|
||||
this.userModifiedDate = new Date(this.immutableItem.serverUpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.immutablePayload.copy({
|
||||
content: this.mutableContent,
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public override setBeginSync(began: Date, globalDirtyIndex: number) {
|
||||
this.immutablePayload = this.immutablePayload.copy({
|
||||
content: this.mutableContent,
|
||||
lastSyncBegan: began,
|
||||
globalDirtyIndexAtLastSync: globalDirtyIndex,
|
||||
})
|
||||
}
|
||||
|
||||
/** Not recommended to use as this might break item schema if used incorrectly */
|
||||
public setCustomContent(content: C): void {
|
||||
this.mutableContent = Copy(content)
|
||||
}
|
||||
|
||||
public set userModifiedDate(date: Date) {
|
||||
this.setAppDataItem(AppDataField.UserModifiedDate, date)
|
||||
}
|
||||
|
||||
public set conflictOf(conflictOf: Uuid | undefined) {
|
||||
this.mutableContent.conflict_of = conflictOf
|
||||
}
|
||||
|
||||
public set protected(isProtected: boolean) {
|
||||
this.mutableContent.protected = isProtected
|
||||
}
|
||||
|
||||
public set trashed(trashed: boolean) {
|
||||
this.mutableContent.trashed = trashed
|
||||
}
|
||||
|
||||
public set pinned(pinned: boolean) {
|
||||
this.setAppDataItem(AppDataField.Pinned, pinned)
|
||||
}
|
||||
|
||||
public set archived(archived: boolean) {
|
||||
this.setAppDataItem(AppDataField.Archived, archived)
|
||||
}
|
||||
|
||||
public set locked(locked: boolean) {
|
||||
this.setAppDataItem(AppDataField.Locked, locked)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the entirety of this domain's data with the data arg.
|
||||
*/
|
||||
public setDomainData(data: DomainDataValueType, domain: ItemDomainKey): void {
|
||||
if (!this.mutableContent.appData) {
|
||||
this.mutableContent.appData = {
|
||||
[DefaultAppDomain]: {},
|
||||
}
|
||||
}
|
||||
|
||||
this.mutableContent.appData[domain] = data
|
||||
}
|
||||
|
||||
/**
|
||||
* First gets the domain data for the input domain.
|
||||
* Then sets data[key] = value
|
||||
*/
|
||||
public setDomainDataKey(key: keyof DomainDataValueType, value: unknown, domain: ItemDomainKey): void {
|
||||
if (!this.mutableContent.appData) {
|
||||
this.mutableContent.appData = {
|
||||
[DefaultAppDomain]: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.mutableContent.appData[domain]) {
|
||||
this.mutableContent.appData[domain] = {}
|
||||
}
|
||||
|
||||
const domainData = this.mutableContent.appData[domain] as DomainDataValueType
|
||||
domainData[key] = value
|
||||
}
|
||||
|
||||
public setAppDataItem(key: AppDataField | PrefKey, value: unknown) {
|
||||
this.setDomainDataKey(key, value, DefaultAppDomain)
|
||||
}
|
||||
|
||||
public e2ePendingRefactor_addItemAsRelationship(item: DecryptedItemInterface) {
|
||||
const references = this.mutableContent.references || []
|
||||
if (!references.find((r) => r.uuid === item.uuid)) {
|
||||
references.push({
|
||||
uuid: item.uuid,
|
||||
content_type: item.content_type,
|
||||
})
|
||||
}
|
||||
this.mutableContent.references = references
|
||||
}
|
||||
|
||||
public removeItemAsRelationship(item: ItemInterface) {
|
||||
let references = this.mutableContent.references || []
|
||||
references = references.filter((r) => r.uuid !== item.uuid)
|
||||
this.mutableContent.references = references
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { DeletedPayload } from './../../Payload/Implementations/DeletedPayload'
|
||||
import { DeletedPayloadInterface, PayloadInterface } from '../../Payload'
|
||||
import { ItemInterface } from '../Interfaces/ItemInterface'
|
||||
import { ItemMutator } from './ItemMutator'
|
||||
import { MutationType } from '../Types/MutationType'
|
||||
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
|
||||
|
||||
export class DeleteItemMutator<
|
||||
I extends ItemInterface<PayloadInterface> = ItemInterface<PayloadInterface>,
|
||||
> extends ItemMutator<PayloadInterface, I> {
|
||||
public getDeletedResult(): DeletedPayloadInterface {
|
||||
const dirtying = this.type !== MutationType.NonDirtying
|
||||
const result = new DeletedPayload(
|
||||
{
|
||||
...this.immutablePayload.ejected(),
|
||||
deleted: true,
|
||||
content: undefined,
|
||||
dirty: dirtying ? true : this.immutablePayload.dirty,
|
||||
dirtyIndex: dirtying ? getIncrementedDirtyIndex() : this.immutablePayload.dirtyIndex,
|
||||
},
|
||||
this.immutablePayload.source,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public override getResult(): PayloadInterface {
|
||||
throw Error('Must use getDeletedResult')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { MutationType } from '../Types/MutationType'
|
||||
import { PayloadInterface } from '../../Payload'
|
||||
import { ItemInterface } from '../Interfaces/ItemInterface'
|
||||
import { TransferPayload } from '../../TransferPayload'
|
||||
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
|
||||
|
||||
/**
|
||||
* An item mutator takes in an item, and an operation, and returns the resulting payload.
|
||||
* Subclasses of mutators can modify the content field directly, but cannot modify the payload directly.
|
||||
* All changes to the payload must occur by copying the payload and reassigning its value.
|
||||
*/
|
||||
export class ItemMutator<
|
||||
P extends PayloadInterface<TransferPayload> = PayloadInterface<TransferPayload>,
|
||||
I extends ItemInterface<P> = ItemInterface<P>,
|
||||
> {
|
||||
public readonly immutableItem: I
|
||||
protected immutablePayload: P
|
||||
protected readonly type: MutationType
|
||||
|
||||
constructor(item: I, type: MutationType) {
|
||||
this.immutableItem = item
|
||||
this.type = type
|
||||
this.immutablePayload = item.payload
|
||||
}
|
||||
|
||||
public getUuid() {
|
||||
return this.immutablePayload.uuid
|
||||
}
|
||||
|
||||
public getItem(): I {
|
||||
return this.immutableItem
|
||||
}
|
||||
|
||||
public getResult(): P {
|
||||
if (this.type === MutationType.NonDirtying) {
|
||||
return this.immutablePayload.copy()
|
||||
}
|
||||
|
||||
const result = this.immutablePayload.copy({
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public setBeginSync(began: Date, globalDirtyIndex: number) {
|
||||
this.immutablePayload = this.immutablePayload.copy({
|
||||
lastSyncBegan: began,
|
||||
globalDirtyIndexAtLastSync: globalDirtyIndex,
|
||||
})
|
||||
}
|
||||
|
||||
public set errorDecrypting(_: boolean) {
|
||||
throw Error('This method is no longer implemented')
|
||||
}
|
||||
|
||||
public set updated_at(_: Date) {
|
||||
throw Error('This method is no longer implemented')
|
||||
}
|
||||
|
||||
public set updated_at_timestamp(_: number) {
|
||||
throw Error('This method is no longer implemented')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum AppDataField {
|
||||
Pinned = 'pinned',
|
||||
Archived = 'archived',
|
||||
Locked = 'locked',
|
||||
UserModifiedDate = 'client_updated_at',
|
||||
DefaultEditor = 'defaultEditor',
|
||||
MobileRules = 'mobileRules',
|
||||
NotAvailableOnMobile = 'notAvailableOnMobile',
|
||||
MobileActive = 'mobileActive',
|
||||
LastSize = 'lastSize',
|
||||
PrefersPlainEditor = 'prefersPlainEditor',
|
||||
ComponentInstallError = 'installError',
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum ConflictStrategy {
|
||||
KeepBase = 1,
|
||||
KeepApply = 2,
|
||||
KeepBaseDuplicateApply = 3,
|
||||
DuplicateBaseKeepApply = 4,
|
||||
KeepBaseMergeRefs = 5,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
|
||||
import { AppDataField } from './AppDataField'
|
||||
|
||||
export const DefaultAppDomain = 'org.standardnotes.sn'
|
||||
/* This domain will be used to save context item client data */
|
||||
export const ComponentDataDomain = 'org.standardnotes.sn.components'
|
||||
|
||||
export type ItemDomainKey = typeof DefaultAppDomain | typeof ComponentDataDomain
|
||||
|
||||
export type AppDomainValueType = Partial<Record<AppDataField | PrefKey, unknown>>
|
||||
export type ComponentDomainValueType = Record<string, unknown>
|
||||
export type DomainDataValueType = AppDomainValueType | ComponentDomainValueType
|
||||
|
||||
export type AppData = {
|
||||
[DefaultAppDomain]: AppDomainValueType
|
||||
[ComponentDataDomain]?: ComponentDomainValueType
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export enum MutationType {
|
||||
UpdateUserTimestamps = 1,
|
||||
/**
|
||||
* The item was changed as part of an internal operation, such as a migration, or, a user
|
||||
* interaction that shouldn't modify timestamps (pinning, protecting, etc).
|
||||
*/
|
||||
NoUpdateUserTimestamps = 2,
|
||||
/**
|
||||
* The item was changed as part of an internal function that wishes to modify
|
||||
* internal item properties, such as lastSyncBegan, without modifying the item's dirty
|
||||
* state. By default all other mutation types will result in a dirtied result.
|
||||
*/
|
||||
NonDirtying = 3,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum SingletonStrategy {
|
||||
KeepEarliest = 1,
|
||||
}
|
||||
29
packages/models/src/Domain/Abstract/Item/index.ts
Normal file
29
packages/models/src/Domain/Abstract/Item/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export * from '../Reference/AnonymousReference'
|
||||
export * from '../Reference/ContenteReferenceType'
|
||||
export * from '../Reference/ContentReference'
|
||||
export * from '../Reference/FileToNoteReference'
|
||||
export * from '../Reference/Functions'
|
||||
export * from '../Reference/LegacyAnonymousReference'
|
||||
export * from '../Reference/LegacyTagToNoteReference'
|
||||
export * from '../Reference/Reference'
|
||||
export * from '../Reference/TagToParentTagReference'
|
||||
export * from './Implementations/DecryptedItem'
|
||||
export * from './Implementations/DecryptedItem'
|
||||
export * from './Implementations/DeletedItem'
|
||||
export * from './Implementations/EncryptedItem'
|
||||
export * from './Implementations/GenericItem'
|
||||
export * from './Interfaces/DecryptedItem'
|
||||
export * from './Interfaces/DeletedItem'
|
||||
export * from './Interfaces/EncryptedItem'
|
||||
export * from './Interfaces/ItemInterface'
|
||||
export * from './Interfaces/TypeCheck'
|
||||
export * from './Mutator/DecryptedItemMutator'
|
||||
export * from './Mutator/DeleteMutator'
|
||||
export * from './Mutator/ItemMutator'
|
||||
export * from './Types/AppDataField'
|
||||
export * from './Types/AppDataField'
|
||||
export * from './Types/ConflictStrategy'
|
||||
export * from './Types/DefaultAppDomain'
|
||||
export * from './Types/DefaultAppDomain'
|
||||
export * from './Types/MutationType'
|
||||
export * from './Types/SingletonStrategy'
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { Copy } from '@standardnotes/utils'
|
||||
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
|
||||
import { FillItemContent, ItemContent } from '../../Content/ItemContent'
|
||||
import { ContentReference } from '../../Reference/ContentReference'
|
||||
import { DecryptedTransferPayload } from '../../TransferPayload/Interfaces/DecryptedTransferPayload'
|
||||
import { DecryptedPayloadInterface } from '../Interfaces/DecryptedPayload'
|
||||
import { PayloadSource } from '../Types/PayloadSource'
|
||||
import { PurePayload } from './PurePayload'
|
||||
|
||||
export class DecryptedPayload<
|
||||
C extends ItemContent = ItemContent,
|
||||
T extends DecryptedTransferPayload<C> = DecryptedTransferPayload<C>,
|
||||
>
|
||||
extends PurePayload<T>
|
||||
implements DecryptedPayloadInterface<C>
|
||||
{
|
||||
override readonly content: C
|
||||
override readonly deleted: false
|
||||
|
||||
constructor(rawPayload: T, source = PayloadSource.Constructor) {
|
||||
super(rawPayload, source)
|
||||
|
||||
this.content = Copy(FillItemContent<C>(rawPayload.content))
|
||||
this.deleted = false
|
||||
}
|
||||
|
||||
get references(): ContentReference[] {
|
||||
return this.content.references || []
|
||||
}
|
||||
|
||||
public getReference(uuid: Uuid): ContentReference {
|
||||
const result = this.references.find((ref) => ref.uuid === uuid)
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Reference not found')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override ejected(): DecryptedTransferPayload<C> {
|
||||
return {
|
||||
...super.ejected(),
|
||||
content: this.content,
|
||||
deleted: this.deleted,
|
||||
}
|
||||
}
|
||||
|
||||
copy(override?: Partial<T>, source = this.source): this {
|
||||
const result = new DecryptedPayload(
|
||||
{
|
||||
...this.ejected(),
|
||||
...override,
|
||||
},
|
||||
source,
|
||||
)
|
||||
return result as this
|
||||
}
|
||||
|
||||
copyAsSyncResolved(override?: Partial<T> & SyncResolvedParams, source = this.source): SyncResolvedPayload {
|
||||
const result = new DecryptedPayload(
|
||||
{
|
||||
...this.ejected(),
|
||||
...override,
|
||||
},
|
||||
source,
|
||||
)
|
||||
return result as SyncResolvedPayload
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { DeletedTransferPayload } from './../../TransferPayload/Interfaces/DeletedTransferPayload'
|
||||
import { DeletedPayloadInterface } from '../Interfaces/DeletedPayload'
|
||||
import { PayloadSource } from '../Types/PayloadSource'
|
||||
import { PurePayload } from './PurePayload'
|
||||
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
|
||||
|
||||
export class DeletedPayload extends PurePayload<DeletedTransferPayload> implements DeletedPayloadInterface {
|
||||
override readonly deleted: true
|
||||
override readonly content: undefined
|
||||
|
||||
constructor(rawPayload: DeletedTransferPayload, source = PayloadSource.Constructor) {
|
||||
super(rawPayload, source)
|
||||
|
||||
this.deleted = true
|
||||
this.content = undefined
|
||||
}
|
||||
|
||||
get discardable(): boolean | undefined {
|
||||
return !this.dirty
|
||||
}
|
||||
|
||||
override ejected(): DeletedTransferPayload {
|
||||
return {
|
||||
...super.ejected(),
|
||||
deleted: this.deleted,
|
||||
content: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
copy(override?: Partial<DeletedTransferPayload>, source = this.source): this {
|
||||
const result = new DeletedPayload(
|
||||
{
|
||||
...this.ejected(),
|
||||
...override,
|
||||
},
|
||||
source,
|
||||
)
|
||||
return result as this
|
||||
}
|
||||
|
||||
copyAsSyncResolved(
|
||||
override?: Partial<DeletedTransferPayload> & SyncResolvedParams,
|
||||
source = this.source,
|
||||
): SyncResolvedPayload {
|
||||
const result = new DeletedPayload(
|
||||
{
|
||||
...this.ejected(),
|
||||
...override,
|
||||
},
|
||||
source,
|
||||
)
|
||||
return result as SyncResolvedPayload
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ProtocolVersion, protocolVersionFromEncryptedString } from '@standardnotes/common'
|
||||
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
|
||||
import { EncryptedTransferPayload } from '../../TransferPayload/Interfaces/EncryptedTransferPayload'
|
||||
import { EncryptedPayloadInterface } from '../Interfaces/EncryptedPayload'
|
||||
import { PayloadSource } from '../Types/PayloadSource'
|
||||
import { PurePayload } from './PurePayload'
|
||||
|
||||
export class EncryptedPayload extends PurePayload<EncryptedTransferPayload> implements EncryptedPayloadInterface {
|
||||
override readonly content: string
|
||||
override readonly deleted: false
|
||||
readonly auth_hash?: string
|
||||
readonly enc_item_key: string
|
||||
readonly errorDecrypting: boolean
|
||||
readonly items_key_id: string | undefined
|
||||
readonly version: ProtocolVersion
|
||||
readonly waitingForKey: boolean
|
||||
|
||||
constructor(rawPayload: EncryptedTransferPayload, source = PayloadSource.Constructor) {
|
||||
super(rawPayload, source)
|
||||
|
||||
this.auth_hash = rawPayload.auth_hash
|
||||
this.content = rawPayload.content
|
||||
this.deleted = false
|
||||
this.enc_item_key = rawPayload.enc_item_key
|
||||
this.errorDecrypting = rawPayload.errorDecrypting
|
||||
this.items_key_id = rawPayload.items_key_id
|
||||
this.version = protocolVersionFromEncryptedString(this.content)
|
||||
this.waitingForKey = rawPayload.waitingForKey
|
||||
}
|
||||
|
||||
override ejected(): EncryptedTransferPayload {
|
||||
return {
|
||||
...super.ejected(),
|
||||
enc_item_key: this.enc_item_key,
|
||||
items_key_id: this.items_key_id,
|
||||
auth_hash: this.auth_hash,
|
||||
errorDecrypting: this.errorDecrypting,
|
||||
waitingForKey: this.waitingForKey,
|
||||
content: this.content,
|
||||
deleted: this.deleted,
|
||||
}
|
||||
}
|
||||
|
||||
copy(override?: Partial<EncryptedTransferPayload>, source = this.source): this {
|
||||
const result = new EncryptedPayload(
|
||||
{
|
||||
...this.ejected(),
|
||||
...override,
|
||||
},
|
||||
source,
|
||||
)
|
||||
return result as this
|
||||
}
|
||||
|
||||
copyAsSyncResolved(
|
||||
override?: Partial<EncryptedTransferPayload> & SyncResolvedParams,
|
||||
source = this.source,
|
||||
): SyncResolvedPayload {
|
||||
const result = new EncryptedPayload(
|
||||
{
|
||||
...this.ejected(),
|
||||
...override,
|
||||
},
|
||||
source,
|
||||
)
|
||||
return result as SyncResolvedPayload
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { deepFreeze, useBoolean } from '@standardnotes/utils'
|
||||
import { PayloadInterface } from '../Interfaces/PayloadInterface'
|
||||
import { PayloadSource } from '../Types/PayloadSource'
|
||||
import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload'
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
|
||||
|
||||
type RequiredKeepUndefined<T> = { [K in keyof T]-?: [T[K]] } extends infer U
|
||||
? U extends Record<keyof U, [unknown]>
|
||||
? { [K in keyof U]: U[K][0] }
|
||||
: never
|
||||
: never
|
||||
|
||||
export abstract class PurePayload<T extends TransferPayload<C>, C extends ItemContent = ItemContent>
|
||||
implements PayloadInterface<T>
|
||||
{
|
||||
readonly source: PayloadSource
|
||||
readonly uuid: string
|
||||
readonly content_type: ContentType
|
||||
readonly deleted: boolean
|
||||
readonly content: C | string | undefined
|
||||
|
||||
readonly created_at: Date
|
||||
readonly updated_at: Date
|
||||
readonly created_at_timestamp: number
|
||||
readonly updated_at_timestamp: number
|
||||
readonly dirtyIndex?: number
|
||||
readonly globalDirtyIndexAtLastSync?: number
|
||||
readonly dirty?: boolean
|
||||
|
||||
readonly lastSyncBegan?: Date
|
||||
readonly lastSyncEnd?: Date
|
||||
|
||||
readonly duplicate_of?: string
|
||||
|
||||
constructor(rawPayload: T, source = PayloadSource.Constructor) {
|
||||
this.source = source
|
||||
this.uuid = rawPayload.uuid
|
||||
|
||||
if (!this.uuid) {
|
||||
throw Error(
|
||||
`Attempting to construct payload with null uuid
|
||||
Content type: ${rawPayload.content_type}`,
|
||||
)
|
||||
}
|
||||
|
||||
this.content = rawPayload.content
|
||||
this.content_type = rawPayload.content_type
|
||||
this.deleted = useBoolean(rawPayload.deleted, false)
|
||||
this.dirty = rawPayload.dirty
|
||||
this.duplicate_of = rawPayload.duplicate_of
|
||||
|
||||
this.created_at = new Date(rawPayload.created_at || new Date())
|
||||
this.updated_at = new Date(rawPayload.updated_at || 0)
|
||||
|
||||
this.created_at_timestamp = rawPayload.created_at_timestamp || 0
|
||||
this.updated_at_timestamp = rawPayload.updated_at_timestamp || 0
|
||||
|
||||
this.lastSyncBegan = rawPayload.lastSyncBegan ? new Date(rawPayload.lastSyncBegan) : undefined
|
||||
this.lastSyncEnd = rawPayload.lastSyncEnd ? new Date(rawPayload.lastSyncEnd) : undefined
|
||||
|
||||
this.dirtyIndex = rawPayload.dirtyIndex
|
||||
this.globalDirtyIndexAtLastSync = rawPayload.globalDirtyIndexAtLastSync
|
||||
|
||||
const timeToAllowSubclassesToFinishConstruction = 0
|
||||
setTimeout(() => {
|
||||
deepFreeze(this)
|
||||
}, timeToAllowSubclassesToFinishConstruction)
|
||||
}
|
||||
|
||||
ejected(): TransferPayload {
|
||||
const comprehensive: RequiredKeepUndefined<TransferPayload> = {
|
||||
uuid: this.uuid,
|
||||
content: this.content,
|
||||
deleted: this.deleted,
|
||||
content_type: this.content_type,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
created_at_timestamp: this.created_at_timestamp,
|
||||
updated_at_timestamp: this.updated_at_timestamp,
|
||||
dirty: this.dirty,
|
||||
duplicate_of: this.duplicate_of,
|
||||
dirtyIndex: this.dirtyIndex,
|
||||
globalDirtyIndexAtLastSync: this.globalDirtyIndexAtLastSync,
|
||||
lastSyncBegan: this.lastSyncBegan,
|
||||
lastSyncEnd: this.lastSyncEnd,
|
||||
}
|
||||
|
||||
return comprehensive
|
||||
}
|
||||
|
||||
public get serverUpdatedAt(): Date {
|
||||
return this.updated_at
|
||||
}
|
||||
|
||||
public get serverUpdatedAtTimestamp(): number {
|
||||
return this.updated_at_timestamp
|
||||
}
|
||||
|
||||
abstract copy(override?: Partial<TransferPayload>, source?: PayloadSource): this
|
||||
|
||||
abstract copyAsSyncResolved(override?: Partial<T> & SyncResolvedParams, source?: PayloadSource): SyncResolvedPayload
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { DecryptedTransferPayload } from './../../TransferPayload/Interfaces/DecryptedTransferPayload'
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { ContentReference } from '../../Reference/ContentReference'
|
||||
import { PayloadInterface } from './PayloadInterface'
|
||||
|
||||
export interface DecryptedPayloadInterface<C extends ItemContent = ItemContent>
|
||||
extends PayloadInterface<DecryptedTransferPayload> {
|
||||
readonly content: C
|
||||
deleted: false
|
||||
|
||||
ejected(): DecryptedTransferPayload<C>
|
||||
get references(): ContentReference[]
|
||||
getReference(uuid: Uuid): ContentReference
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DeletedTransferPayload } from '../../TransferPayload'
|
||||
import { PayloadInterface } from './PayloadInterface'
|
||||
|
||||
export interface DeletedPayloadInterface extends PayloadInterface<DeletedTransferPayload> {
|
||||
readonly deleted: true
|
||||
readonly content: undefined
|
||||
|
||||
/**
|
||||
* Whether a payload can be discarded and removed from storage.
|
||||
* This value is true if a payload is marked as deleted and not dirty.
|
||||
*/
|
||||
discardable: boolean | undefined
|
||||
|
||||
ejected(): DeletedTransferPayload
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { EncryptedTransferPayload } from '../../TransferPayload/Interfaces/EncryptedTransferPayload'
|
||||
import { PayloadInterface } from './PayloadInterface'
|
||||
|
||||
export interface EncryptedPayloadInterface extends PayloadInterface<EncryptedTransferPayload> {
|
||||
readonly content: string
|
||||
readonly deleted: false
|
||||
readonly enc_item_key: string
|
||||
readonly items_key_id: string | undefined
|
||||
readonly errorDecrypting: boolean
|
||||
readonly waitingForKey: boolean
|
||||
readonly version: ProtocolVersion
|
||||
|
||||
/** @deprecated */
|
||||
readonly auth_hash?: string
|
||||
|
||||
ejected(): EncryptedTransferPayload
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { SyncResolvedParams, SyncResolvedPayload } from './../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload'
|
||||
import { PayloadSource } from '../Types/PayloadSource'
|
||||
|
||||
export interface PayloadInterface<T extends TransferPayload = TransferPayload, C extends ItemContent = ItemContent> {
|
||||
readonly source: PayloadSource
|
||||
readonly uuid: Uuid
|
||||
readonly content_type: ContentType
|
||||
content: C | string | undefined
|
||||
deleted: boolean
|
||||
|
||||
/** updated_at is set by the server only, and not the client.*/
|
||||
readonly updated_at: Date
|
||||
readonly created_at: Date
|
||||
readonly created_at_timestamp: number
|
||||
readonly updated_at_timestamp: number
|
||||
get serverUpdatedAt(): Date
|
||||
get serverUpdatedAtTimestamp(): number
|
||||
|
||||
readonly dirtyIndex?: number
|
||||
readonly globalDirtyIndexAtLastSync?: number
|
||||
readonly dirty?: boolean
|
||||
|
||||
readonly lastSyncBegan?: Date
|
||||
readonly lastSyncEnd?: Date
|
||||
|
||||
readonly duplicate_of?: Uuid
|
||||
|
||||
/**
|
||||
* "Ejected" means a payload for
|
||||
* generic, non-contextual consumption, such as saving to a backup file or syncing
|
||||
* with a server.
|
||||
*/
|
||||
ejected(): TransferPayload
|
||||
|
||||
copy(override?: Partial<T>, source?: PayloadSource): this
|
||||
|
||||
copyAsSyncResolved(override?: Partial<T> & SyncResolvedParams, source?: PayloadSource): SyncResolvedPayload
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import {
|
||||
isDecryptedTransferPayload,
|
||||
isDeletedTransferPayload,
|
||||
isEncryptedTransferPayload,
|
||||
isErrorDecryptingTransferPayload,
|
||||
} from '../../TransferPayload'
|
||||
import { DecryptedPayloadInterface } from './DecryptedPayload'
|
||||
import { DeletedPayloadInterface } from './DeletedPayload'
|
||||
import { EncryptedPayloadInterface } from './EncryptedPayload'
|
||||
import { PayloadInterface } from './PayloadInterface'
|
||||
|
||||
export function isDecryptedPayload<C extends ItemContent = ItemContent>(
|
||||
payload: PayloadInterface,
|
||||
): payload is DecryptedPayloadInterface<C> {
|
||||
return isDecryptedTransferPayload(payload)
|
||||
}
|
||||
|
||||
export function isEncryptedPayload(payload: PayloadInterface): payload is EncryptedPayloadInterface {
|
||||
return isEncryptedTransferPayload(payload)
|
||||
}
|
||||
|
||||
export function isDeletedPayload(payload: PayloadInterface): payload is DeletedPayloadInterface {
|
||||
return isDeletedTransferPayload(payload)
|
||||
}
|
||||
|
||||
export function isErrorDecryptingPayload(payload: PayloadInterface): payload is EncryptedPayloadInterface {
|
||||
return isErrorDecryptingTransferPayload(payload)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { DecryptedPayloadInterface } from './DecryptedPayload'
|
||||
import { DeletedPayloadInterface } from './DeletedPayload'
|
||||
import { EncryptedPayloadInterface } from './EncryptedPayload'
|
||||
|
||||
export type FullyFormedPayloadInterface<C extends ItemContent = ItemContent> =
|
||||
| DecryptedPayloadInterface<C>
|
||||
| EncryptedPayloadInterface
|
||||
| DeletedPayloadInterface
|
||||
|
||||
export type AnyNonDecryptedPayloadInterface = EncryptedPayloadInterface | DeletedPayloadInterface
|
||||
@@ -0,0 +1,43 @@
|
||||
export enum PayloadEmitSource {
|
||||
/** When an observer registers to stream items, the items are pushed immediately to the observer */
|
||||
InitialObserverRegistrationPush = 1,
|
||||
|
||||
/**
|
||||
* Payload when a client modifies item property then maps it to update UI.
|
||||
* This also indicates that the item was dirtied
|
||||
*/
|
||||
LocalChanged,
|
||||
LocalInserted,
|
||||
LocalDatabaseLoaded,
|
||||
/** The payload returned by offline sync operation */
|
||||
OfflineSyncSaved,
|
||||
LocalRetrieved,
|
||||
|
||||
FileImport,
|
||||
|
||||
ComponentRetrieved,
|
||||
/** Payloads received from an external component with the intention of creating a new item */
|
||||
ComponentCreated,
|
||||
|
||||
/**
|
||||
* When the payloads are about to sync, they are emitted by the sync service with updated
|
||||
* values of lastSyncBegan. Payloads emitted from this source indicate that these payloads
|
||||
* have been saved to disk, and are about to be synced
|
||||
*/
|
||||
PreSyncSave,
|
||||
|
||||
RemoteRetrieved,
|
||||
RemoteSaved,
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the changed payload represents only an internal change that shouldn't
|
||||
* require a UI refresh
|
||||
*/
|
||||
export function isPayloadSourceInternalChange(source: PayloadEmitSource): boolean {
|
||||
return [PayloadEmitSource.RemoteSaved, PayloadEmitSource.PreSyncSave].includes(source)
|
||||
}
|
||||
|
||||
export function isPayloadSourceRetrieved(source: PayloadEmitSource): boolean {
|
||||
return [PayloadEmitSource.RemoteRetrieved, PayloadEmitSource.ComponentRetrieved].includes(source)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum PayloadSource {
|
||||
/**
|
||||
* Payloads with a source of Constructor means that the payload was created
|
||||
* in isolated space by the caller, and does not yet have any app-related affiliation.
|
||||
*/
|
||||
Constructor = 1,
|
||||
|
||||
RemoteRetrieved,
|
||||
|
||||
RemoteSaved,
|
||||
|
||||
FileImport,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function PayloadTimestampDefaults() {
|
||||
return {
|
||||
updated_at: new Date(0),
|
||||
created_at: new Date(),
|
||||
updated_at_timestamp: 0,
|
||||
created_at_timestamp: 0,
|
||||
}
|
||||
}
|
||||
13
packages/models/src/Domain/Abstract/Payload/index.ts
Normal file
13
packages/models/src/Domain/Abstract/Payload/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from './Implementations/PurePayload'
|
||||
export * from './Implementations/DecryptedPayload'
|
||||
export * from './Implementations/EncryptedPayload'
|
||||
export * from './Implementations/DeletedPayload'
|
||||
export * from './Interfaces/DecryptedPayload'
|
||||
export * from './Interfaces/DeletedPayload'
|
||||
export * from './Interfaces/EncryptedPayload'
|
||||
export * from './Interfaces/PayloadInterface'
|
||||
export * from './Interfaces/TypeCheck'
|
||||
export * from './Interfaces/UnionTypes'
|
||||
export * from './Types/PayloadSource'
|
||||
export * from './Types/EmitSource'
|
||||
export * from './Types/TimestampDefaults'
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ContenteReferenceType } from './ContenteReferenceType'
|
||||
|
||||
export interface AnonymousReference {
|
||||
uuid: string
|
||||
content_type: ContentType
|
||||
reference_type: ContenteReferenceType
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
|
||||
import { Reference } from './Reference'
|
||||
|
||||
export type ContentReference = LegacyAnonymousReference | Reference
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum ContenteReferenceType {
|
||||
TagToParentTag = 'TagToParentTag',
|
||||
FileToNote = 'FileToNote',
|
||||
TagToFile = 'TagToFile',
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { AnonymousReference } from './AnonymousReference'
|
||||
import { ContenteReferenceType } from './ContenteReferenceType'
|
||||
|
||||
export interface FileToNoteReference extends AnonymousReference {
|
||||
content_type: ContentType.Note
|
||||
reference_type: ContenteReferenceType.FileToNote
|
||||
}
|
||||
30
packages/models/src/Domain/Abstract/Reference/Functions.ts
Normal file
30
packages/models/src/Domain/Abstract/Reference/Functions.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ItemInterface } from '../Item/Interfaces/ItemInterface'
|
||||
import { ContenteReferenceType } from './ContenteReferenceType'
|
||||
import { ContentReference } from './ContentReference'
|
||||
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
|
||||
import { LegacyTagToNoteReference } from './LegacyTagToNoteReference'
|
||||
import { Reference } from './Reference'
|
||||
import { TagToParentTagReference } from './TagToParentTagReference'
|
||||
|
||||
export const isLegacyAnonymousReference = (x: ContentReference): x is LegacyAnonymousReference => {
|
||||
return (x as any).reference_type === undefined
|
||||
}
|
||||
|
||||
export const isReference = (x: ContentReference): x is Reference => {
|
||||
return (x as any).reference_type !== undefined
|
||||
}
|
||||
|
||||
export const isLegacyTagToNoteReference = (
|
||||
x: LegacyAnonymousReference,
|
||||
currentItem: ItemInterface,
|
||||
): x is LegacyTagToNoteReference => {
|
||||
const isReferenceToANote = x.content_type === ContentType.Note
|
||||
const isReferenceFromATag = currentItem.content_type === ContentType.Tag
|
||||
return isReferenceToANote && isReferenceFromATag
|
||||
}
|
||||
|
||||
export const isTagToParentTagReference = (x: ContentReference): x is TagToParentTagReference => {
|
||||
return isReference(x) && x.reference_type === ContenteReferenceType.TagToParentTag
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface LegacyAnonymousReference {
|
||||
uuid: string
|
||||
content_type: string
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
|
||||
|
||||
export interface LegacyTagToNoteReference extends LegacyAnonymousReference {
|
||||
content_type: ContentType.Note
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TagToParentTagReference } from './TagToParentTagReference'
|
||||
|
||||
export type Reference = TagToParentTagReference
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { AnonymousReference } from './AnonymousReference'
|
||||
import { ContenteReferenceType } from './ContenteReferenceType'
|
||||
|
||||
export interface TagToFileReference extends AnonymousReference {
|
||||
content_type: ContentType.File
|
||||
reference_type: ContenteReferenceType.TagToFile
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { AnonymousReference } from './AnonymousReference'
|
||||
import { ContenteReferenceType } from './ContenteReferenceType'
|
||||
|
||||
export interface TagToParentTagReference extends AnonymousReference {
|
||||
content_type: ContentType.Tag
|
||||
reference_type: ContenteReferenceType.TagToParentTag
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { TransferPayload } from './TransferPayload'
|
||||
|
||||
export interface DecryptedTransferPayload<C extends ItemContent = ItemContent> extends TransferPayload {
|
||||
content: C
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { TransferPayload } from './TransferPayload'
|
||||
|
||||
export interface DeletedTransferPayload extends TransferPayload {
|
||||
content: undefined
|
||||
deleted: true
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TransferPayload } from './TransferPayload'
|
||||
|
||||
export interface EncryptedTransferPayload extends TransferPayload {
|
||||
content: string
|
||||
enc_item_key: string
|
||||
items_key_id: string | undefined
|
||||
errorDecrypting: boolean
|
||||
waitingForKey: boolean
|
||||
/** @deprecated */
|
||||
auth_hash?: string
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
|
||||
export interface TransferPayload<C extends ItemContent = ItemContent> {
|
||||
uuid: Uuid
|
||||
content_type: ContentType
|
||||
content: C | string | undefined
|
||||
deleted?: boolean
|
||||
|
||||
updated_at: Date
|
||||
created_at: Date
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
|
||||
dirtyIndex?: number
|
||||
globalDirtyIndexAtLastSync?: number
|
||||
dirty?: boolean
|
||||
|
||||
lastSyncBegan?: Date
|
||||
lastSyncEnd?: Date
|
||||
|
||||
duplicate_of?: Uuid
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { isObject, isString } from '@standardnotes/utils'
|
||||
import { DecryptedTransferPayload } from './DecryptedTransferPayload'
|
||||
import { DeletedTransferPayload } from './DeletedTransferPayload'
|
||||
import { EncryptedTransferPayload } from './EncryptedTransferPayload'
|
||||
import { TransferPayload } from './TransferPayload'
|
||||
|
||||
export type FullyFormedTransferPayload = DecryptedTransferPayload | EncryptedTransferPayload | DeletedTransferPayload
|
||||
|
||||
export function isDecryptedTransferPayload(payload: TransferPayload): payload is DecryptedTransferPayload {
|
||||
return isObject(payload.content)
|
||||
}
|
||||
|
||||
export function isEncryptedTransferPayload(payload: TransferPayload): payload is EncryptedTransferPayload {
|
||||
return 'content' in payload && isString(payload.content)
|
||||
}
|
||||
|
||||
export function isErrorDecryptingTransferPayload(payload: TransferPayload): payload is EncryptedTransferPayload {
|
||||
return isEncryptedTransferPayload(payload) && payload.errorDecrypting === true
|
||||
}
|
||||
|
||||
export function isDeletedTransferPayload(payload: TransferPayload): payload is DeletedTransferPayload {
|
||||
return 'deleted' in payload && payload.deleted === true
|
||||
}
|
||||
|
||||
export function isCorruptTransferPayload(payload: TransferPayload): boolean {
|
||||
const invalidDeletedState = payload.deleted === true && payload.content != undefined
|
||||
return payload.uuid == undefined || invalidDeletedState
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './Interfaces/DecryptedTransferPayload'
|
||||
export * from './Interfaces/DeletedTransferPayload'
|
||||
export * from './Interfaces/EncryptedTransferPayload'
|
||||
export * from './Interfaces/TransferPayload'
|
||||
export * from './Interfaces/TypeCheck'
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
KeyParamsContent001,
|
||||
KeyParamsContent002,
|
||||
KeyParamsContent003,
|
||||
KeyParamsContent004,
|
||||
AnyKeyParamsContent,
|
||||
ProtocolVersion,
|
||||
KeyParamsOrigination,
|
||||
} from '@standardnotes/common'
|
||||
|
||||
/**
|
||||
* Key params are public data that contain information about how a root key was created.
|
||||
* Given a keyParams object and a password, clients can compute a root key that was created
|
||||
* previously.
|
||||
*/
|
||||
export interface RootKeyParamsInterface {
|
||||
readonly content: AnyKeyParamsContent
|
||||
|
||||
/**
|
||||
* For consumers to determine whether the object they are
|
||||
* working with is a proper RootKeyParams object.
|
||||
*/
|
||||
get isKeyParamsObject(): boolean
|
||||
|
||||
get identifier(): string
|
||||
|
||||
get version(): ProtocolVersion
|
||||
get origination(): KeyParamsOrigination | undefined
|
||||
|
||||
get content001(): KeyParamsContent001
|
||||
|
||||
get content002(): KeyParamsContent002
|
||||
|
||||
get content003(): KeyParamsContent003
|
||||
|
||||
get content004(): KeyParamsContent004
|
||||
|
||||
get createdDate(): Date | undefined
|
||||
|
||||
compare(other: RootKeyParamsInterface): boolean
|
||||
|
||||
/**
|
||||
* When saving in a file or communicating with server,
|
||||
* use the original values.
|
||||
*/
|
||||
getPortableValue(): AnyKeyParamsContent
|
||||
}
|
||||
31
packages/models/src/Domain/Local/RootKey/KeychainTypes.ts
Normal file
31
packages/models/src/Domain/Local/RootKey/KeychainTypes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ApplicationIdentifier, ProtocolVersion } from '@standardnotes/common'
|
||||
import { RootKeyContentSpecialized } from './RootKeyContent'
|
||||
|
||||
export type RawKeychainValue = Record<ApplicationIdentifier, NamespacedRootKeyInKeychain>
|
||||
|
||||
export interface NamespacedRootKeyInKeychain {
|
||||
version: ProtocolVersion
|
||||
masterKey: string
|
||||
dataAuthenticationKey?: string
|
||||
}
|
||||
|
||||
export type RootKeyContentInStorage = RootKeyContentSpecialized
|
||||
|
||||
export interface LegacyRawKeychainValue {
|
||||
mk: string
|
||||
ak: string
|
||||
version: ProtocolVersion
|
||||
}
|
||||
|
||||
export type LegacyMobileKeychainStructure = {
|
||||
offline?: {
|
||||
timing?: unknown
|
||||
pw?: string
|
||||
}
|
||||
encryptedAccountKeys?: unknown
|
||||
mk: string
|
||||
pw: string
|
||||
ak: string
|
||||
version?: string
|
||||
jwt?: string
|
||||
}
|
||||
12
packages/models/src/Domain/Local/RootKey/RootKeyContent.ts
Normal file
12
packages/models/src/Domain/Local/RootKey/RootKeyContent.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { ProtocolVersion, AnyKeyParamsContent } from '@standardnotes/common'
|
||||
|
||||
export interface RootKeyContentSpecialized {
|
||||
version: ProtocolVersion
|
||||
masterKey: string
|
||||
serverPassword?: string
|
||||
dataAuthenticationKey?: string
|
||||
keyParams: AnyKeyParamsContent
|
||||
}
|
||||
|
||||
export type RootKeyContent = RootKeyContentSpecialized & ItemContent
|
||||
17
packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts
Normal file
17
packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem'
|
||||
import { RootKeyParamsInterface } from '../KeyParams/RootKeyParamsInterface'
|
||||
import { NamespacedRootKeyInKeychain, RootKeyContentInStorage } from './KeychainTypes'
|
||||
import { RootKeyContent } from './RootKeyContent'
|
||||
|
||||
export interface RootKeyInterface extends DecryptedItemInterface<RootKeyContent> {
|
||||
readonly keyParams: RootKeyParamsInterface
|
||||
get keyVersion(): ProtocolVersion
|
||||
get itemsKey(): string
|
||||
get masterKey(): string
|
||||
get serverPassword(): string | undefined
|
||||
get dataAuthenticationKey(): string | undefined
|
||||
compare(otherKey: RootKeyInterface): boolean
|
||||
persistableValueWhenWrapping(): RootKeyContentInStorage
|
||||
getKeychainValue(): NamespacedRootKeyInKeychain
|
||||
}
|
||||
263
packages/models/src/Domain/Runtime/Collection/Collection.ts
Normal file
263
packages/models/src/Domain/Runtime/Collection/Collection.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { extendArray, isObject, isString, UuidMap } from '@standardnotes/utils'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { remove } from 'lodash'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { ContentReference } from '../../Abstract/Item'
|
||||
|
||||
export interface CollectionElement {
|
||||
uuid: Uuid
|
||||
content_type: ContentType
|
||||
dirty?: boolean
|
||||
deleted?: boolean
|
||||
}
|
||||
|
||||
export interface DecryptedCollectionElement<C extends ItemContent = ItemContent> extends CollectionElement {
|
||||
content: C
|
||||
references: ContentReference[]
|
||||
}
|
||||
|
||||
export interface DeletedCollectionElement extends CollectionElement {
|
||||
content: undefined
|
||||
deleted: true
|
||||
}
|
||||
|
||||
export interface EncryptedCollectionElement extends CollectionElement {
|
||||
content: string
|
||||
errorDecrypting: boolean
|
||||
}
|
||||
|
||||
export abstract class Collection<
|
||||
Element extends Decrypted | Encrypted | Deleted,
|
||||
Decrypted extends DecryptedCollectionElement,
|
||||
Encrypted extends EncryptedCollectionElement,
|
||||
Deleted extends DeletedCollectionElement,
|
||||
> {
|
||||
readonly map: Partial<Record<Uuid, Element>> = {}
|
||||
readonly typedMap: Partial<Record<ContentType, Element[]>> = {}
|
||||
|
||||
/** An array of uuids of items that are dirty */
|
||||
dirtyIndex: Set<Uuid> = new Set()
|
||||
|
||||
/** An array of uuids of items that are not marked as deleted */
|
||||
nondeletedIndex: Set<Uuid> = new Set()
|
||||
|
||||
/** An array of uuids of items that are errorDecrypting or waitingForKey */
|
||||
invalidsIndex: Set<Uuid> = new Set()
|
||||
|
||||
readonly referenceMap: UuidMap
|
||||
|
||||
/** Maintains an index for each item uuid where the value is an array of uuids that are
|
||||
* conflicts of that item. So if Note B and C are conflicts of Note A,
|
||||
* conflictMap[A.uuid] == [B.uuid, C.uuid] */
|
||||
readonly conflictMap: UuidMap
|
||||
|
||||
isDecryptedElement = (e: Decrypted | Encrypted | Deleted): e is Decrypted => {
|
||||
return isObject(e.content)
|
||||
}
|
||||
|
||||
isEncryptedElement = (e: Decrypted | Encrypted | Deleted): e is Encrypted => {
|
||||
return 'content' in e && isString(e.content)
|
||||
}
|
||||
|
||||
isErrorDecryptingElement = (e: Decrypted | Encrypted | Deleted): e is Encrypted => {
|
||||
return this.isEncryptedElement(e) && e.errorDecrypting === true
|
||||
}
|
||||
|
||||
isDeletedElement = (e: Decrypted | Encrypted | Deleted): e is Deleted => {
|
||||
return 'deleted' in e && e.deleted === true
|
||||
}
|
||||
|
||||
isNonDeletedElement = (e: Decrypted | Encrypted | Deleted): e is Decrypted | Encrypted => {
|
||||
return !this.isDeletedElement(e)
|
||||
}
|
||||
|
||||
constructor(
|
||||
copy = false,
|
||||
mapCopy?: Partial<Record<Uuid, Element>>,
|
||||
typedMapCopy?: Partial<Record<ContentType, Element[]>>,
|
||||
referenceMapCopy?: UuidMap,
|
||||
conflictMapCopy?: UuidMap,
|
||||
) {
|
||||
if (copy) {
|
||||
this.map = mapCopy!
|
||||
this.typedMap = typedMapCopy!
|
||||
this.referenceMap = referenceMapCopy!
|
||||
this.conflictMap = conflictMapCopy!
|
||||
} else {
|
||||
this.referenceMap = new UuidMap()
|
||||
this.conflictMap = new UuidMap()
|
||||
}
|
||||
}
|
||||
|
||||
public uuids(): Uuid[] {
|
||||
return Object.keys(this.map)
|
||||
}
|
||||
|
||||
public all(contentType?: ContentType | ContentType[]): Element[] {
|
||||
if (contentType) {
|
||||
if (Array.isArray(contentType)) {
|
||||
const elements: Element[] = []
|
||||
for (const type of contentType) {
|
||||
extendArray(elements, this.typedMap[type] || [])
|
||||
}
|
||||
return elements
|
||||
} else {
|
||||
return this.typedMap[contentType]?.slice() || []
|
||||
}
|
||||
} else {
|
||||
return Object.keys(this.map).map((uuid: Uuid) => {
|
||||
return this.map[uuid]
|
||||
}) as Element[]
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns all elements that are not marked as deleted */
|
||||
public nondeletedElements(): Element[] {
|
||||
const uuids = Array.from(this.nondeletedIndex)
|
||||
return this.findAll(uuids).filter(this.isNonDeletedElement)
|
||||
}
|
||||
|
||||
/** Returns all elements that are errorDecrypting or waitingForKey */
|
||||
public invalidElements(): Encrypted[] {
|
||||
const uuids = Array.from(this.invalidsIndex)
|
||||
return this.findAll(uuids) as Encrypted[]
|
||||
}
|
||||
|
||||
/** Returns all elements that are marked as dirty */
|
||||
public dirtyElements(): Element[] {
|
||||
const uuids = Array.from(this.dirtyIndex)
|
||||
return this.findAll(uuids)
|
||||
}
|
||||
|
||||
public findAll(uuids: Uuid[]): Element[] {
|
||||
const results: Element[] = []
|
||||
|
||||
for (const id of uuids) {
|
||||
const element = this.map[id]
|
||||
if (element) {
|
||||
results.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
public find(uuid: Uuid): Element | undefined {
|
||||
return this.map[uuid]
|
||||
}
|
||||
|
||||
public has(uuid: Uuid): boolean {
|
||||
return this.find(uuid) != undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* If an item is not found, an `undefined` element
|
||||
* will be inserted into the array.
|
||||
*/
|
||||
public findAllIncludingBlanks<E extends Element>(uuids: Uuid[]): (E | Deleted | undefined)[] {
|
||||
const results: (E | Deleted | undefined)[] = []
|
||||
|
||||
for (const id of uuids) {
|
||||
const element = this.map[id] as E | Deleted | undefined
|
||||
results.push(element)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
public set(elements: Element | Element[]): void {
|
||||
elements = Array.isArray(elements) ? elements : [elements]
|
||||
|
||||
if (elements.length === 0) {
|
||||
console.warn('Attempting to set 0 elements onto collection')
|
||||
return
|
||||
}
|
||||
|
||||
for (const element of elements) {
|
||||
this.map[element.uuid] = element
|
||||
this.setToTypedMap(element)
|
||||
|
||||
if (this.isErrorDecryptingElement(element)) {
|
||||
this.invalidsIndex.add(element.uuid)
|
||||
} else {
|
||||
this.invalidsIndex.delete(element.uuid)
|
||||
}
|
||||
|
||||
if (this.isDecryptedElement(element)) {
|
||||
const conflictOf = element.content.conflict_of
|
||||
if (conflictOf) {
|
||||
this.conflictMap.establishRelationship(conflictOf, element.uuid)
|
||||
}
|
||||
|
||||
this.referenceMap.setAllRelationships(
|
||||
element.uuid,
|
||||
element.references.map((r) => r.uuid),
|
||||
)
|
||||
}
|
||||
|
||||
if (element.dirty) {
|
||||
this.dirtyIndex.add(element.uuid)
|
||||
} else {
|
||||
this.dirtyIndex.delete(element.uuid)
|
||||
}
|
||||
|
||||
if (element.deleted) {
|
||||
this.nondeletedIndex.delete(element.uuid)
|
||||
} else {
|
||||
this.nondeletedIndex.add(element.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public discard(elements: Element | Element[]): void {
|
||||
elements = Array.isArray(elements) ? elements : [elements]
|
||||
for (const element of elements) {
|
||||
this.deleteFromTypedMap(element)
|
||||
delete this.map[element.uuid]
|
||||
this.conflictMap.removeFromMap(element.uuid)
|
||||
this.referenceMap.removeFromMap(element.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
public uuidReferencesForUuid(uuid: Uuid): Uuid[] {
|
||||
return this.referenceMap.getDirectRelationships(uuid)
|
||||
}
|
||||
|
||||
public uuidsThatReferenceUuid(uuid: Uuid): Uuid[] {
|
||||
return this.referenceMap.getInverseRelationships(uuid)
|
||||
}
|
||||
|
||||
public referencesForElement(element: Decrypted): Element[] {
|
||||
const uuids = this.referenceMap.getDirectRelationships(element.uuid)
|
||||
return this.findAll(uuids)
|
||||
}
|
||||
|
||||
public conflictsOf(uuid: Uuid): Element[] {
|
||||
const uuids = this.conflictMap.getDirectRelationships(uuid)
|
||||
return this.findAll(uuids)
|
||||
}
|
||||
|
||||
public elementsReferencingElement(element: Decrypted, contentType?: ContentType): Element[] {
|
||||
const uuids = this.uuidsThatReferenceUuid(element.uuid)
|
||||
const items = this.findAll(uuids)
|
||||
|
||||
if (!contentType) {
|
||||
return items
|
||||
}
|
||||
|
||||
return items.filter((item) => item.content_type === contentType)
|
||||
}
|
||||
|
||||
private setToTypedMap(element: Element): void {
|
||||
const array = this.typedMap[element.content_type] || []
|
||||
remove(array, { uuid: element.uuid as never })
|
||||
array.push(element)
|
||||
this.typedMap[element.content_type] = array
|
||||
}
|
||||
|
||||
private deleteFromTypedMap(element: Element): void {
|
||||
const array = this.typedMap[element.content_type] || []
|
||||
remove(array, { uuid: element.uuid as never })
|
||||
this.typedMap[element.content_type] = array
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { UuidMap } from '@standardnotes/utils'
|
||||
|
||||
export interface CollectionInterface {
|
||||
/** Maintains an index where the direct map for each item id is an array
|
||||
* of item ids that the item references. This is essentially equivalent to
|
||||
* item.content.references, but keeps state even when the item is deleted.
|
||||
* So if tag A references Note B, referenceMap.directMap[A.uuid] == [B.uuid].
|
||||
* The inverse map for each item is an array of item ids where the items reference the
|
||||
* key item. So if tag A references Note B, referenceMap.inverseMap[B.uuid] == [A.uuid].
|
||||
* This allows callers to determine for a given item, who references it?
|
||||
* It would be prohibitive to look this up on demand */
|
||||
readonly referenceMap: UuidMap
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Uuid, ContentType } from '@standardnotes/common'
|
||||
|
||||
export interface SortableItem {
|
||||
uuid: Uuid
|
||||
content_type: ContentType
|
||||
created_at: Date
|
||||
userModifiedDate: Date
|
||||
title?: string
|
||||
pinned: boolean
|
||||
}
|
||||
|
||||
export const CollectionSort: Record<string, keyof SortableItem> = {
|
||||
CreatedAt: 'created_at',
|
||||
UpdatedAt: 'userModifiedDate',
|
||||
Title: 'title',
|
||||
}
|
||||
|
||||
export type CollectionSortDirection = 'asc' | 'dsc'
|
||||
|
||||
export type CollectionSortProperty = keyof SortableItem
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NoteContent } from './../../../Syncable/Note/NoteContent'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedItem } from '../../../Abstract/Item'
|
||||
import { DecryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { FillItemContent, ItemContent } from '../../../Abstract/Content/ItemContent'
|
||||
|
||||
describe('item collection', () => {
|
||||
const createDecryptedPayload = (uuid?: string): DecryptedPayload => {
|
||||
return new DecryptedPayload({
|
||||
uuid: uuid || String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: FillItemContent<NoteContent>({
|
||||
title: 'foo',
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
}
|
||||
|
||||
it('setting same item twice should not result in doubles', () => {
|
||||
const collection = new ItemCollection()
|
||||
|
||||
const decryptedItem = new DecryptedItem(createDecryptedPayload())
|
||||
collection.set(decryptedItem)
|
||||
|
||||
const updatedItem = new DecryptedItem(
|
||||
decryptedItem.payload.copy({
|
||||
content: { foo: 'bar' } as unknown as jest.Mocked<ItemContent>,
|
||||
}),
|
||||
)
|
||||
|
||||
collection.set(updatedItem)
|
||||
|
||||
expect(collection.all()).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ItemContent } from './../../../Abstract/Content/ItemContent'
|
||||
import { EncryptedItemInterface } from './../../../Abstract/Item/Interfaces/EncryptedItem'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { SNIndex } from '../../Index/SNIndex'
|
||||
import { isDecryptedItem } from '../../../Abstract/Item/Interfaces/TypeCheck'
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item/Interfaces/DecryptedItem'
|
||||
import { CollectionInterface } from '../CollectionInterface'
|
||||
import { DeletedItemInterface } from '../../../Abstract/Item'
|
||||
import { Collection } from '../Collection'
|
||||
import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
|
||||
export class ItemCollection
|
||||
extends Collection<AnyItemInterface, DecryptedItemInterface, EncryptedItemInterface, DeletedItemInterface>
|
||||
implements SNIndex, CollectionInterface
|
||||
{
|
||||
public onChange(delta: ItemDelta): void {
|
||||
const changedOrInserted = delta.changed.concat(delta.inserted)
|
||||
|
||||
if (changedOrInserted.length > 0) {
|
||||
this.set(changedOrInserted)
|
||||
}
|
||||
|
||||
this.discard(delta.discarded)
|
||||
}
|
||||
|
||||
public findDecrypted<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: Uuid): T | undefined {
|
||||
const result = this.find(uuid)
|
||||
|
||||
if (!result) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return isDecryptedItem(result) ? (result as T) : undefined
|
||||
}
|
||||
|
||||
public findAllDecrypted<T extends DecryptedItemInterface = DecryptedItemInterface>(uuids: Uuid[]): T[] {
|
||||
return this.findAll(uuids).filter(isDecryptedItem) as T[]
|
||||
}
|
||||
|
||||
public findAllDecryptedWithBlanks<C extends ItemContent = ItemContent>(
|
||||
uuids: Uuid[],
|
||||
): (DecryptedItemInterface<C> | undefined)[] {
|
||||
const results = this.findAllIncludingBlanks(uuids)
|
||||
const mapped = results.map((i) => {
|
||||
if (i == undefined || isDecryptedItem(i)) {
|
||||
return i
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
return mapped as (DecryptedItemInterface<C> | undefined)[]
|
||||
}
|
||||
|
||||
public allDecrypted<T extends DecryptedItemInterface>(contentType: ContentType | ContentType[]): T[] {
|
||||
return this.all(contentType).filter(isDecryptedItem) as T[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NoteContent } from './../../../Syncable/Note/NoteContent'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedItem, EncryptedItem } from '../../../Abstract/Item'
|
||||
import { DecryptedPayload, EncryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { FillItemContent } from '../../../Abstract/Content/ItemContent'
|
||||
import { TagNotesIndex } from './TagNotesIndex'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes'
|
||||
|
||||
describe('tag notes index', () => {
|
||||
const createEncryptedItem = (uuid?: string) => {
|
||||
const payload = new EncryptedPayload({
|
||||
uuid: uuid || String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: '004:...',
|
||||
enc_item_key: '004:...',
|
||||
items_key_id: '123',
|
||||
waitingForKey: true,
|
||||
errorDecrypting: true,
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
return new EncryptedItem(payload)
|
||||
}
|
||||
|
||||
const createDecryptedItem = (uuid?: string) => {
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: uuid || String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: FillItemContent<NoteContent>({
|
||||
title: 'foo',
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return new DecryptedItem(payload)
|
||||
}
|
||||
|
||||
const createChangeDelta = (item: AnyItemInterface): ItemDelta => {
|
||||
return {
|
||||
changed: [item],
|
||||
inserted: [],
|
||||
discarded: [],
|
||||
ignored: [],
|
||||
unerrored: [],
|
||||
}
|
||||
}
|
||||
|
||||
it('should decrement count after decrypted note becomes errored', () => {
|
||||
const collection = new ItemCollection()
|
||||
const index = new TagNotesIndex(collection)
|
||||
|
||||
const decryptedItem = createDecryptedItem()
|
||||
collection.set(decryptedItem)
|
||||
index.onChange(createChangeDelta(decryptedItem))
|
||||
|
||||
expect(index.allCountableNotesCount()).toEqual(1)
|
||||
|
||||
const encryptedItem = createEncryptedItem(decryptedItem.uuid)
|
||||
collection.set(encryptedItem)
|
||||
index.onChange(createChangeDelta(encryptedItem))
|
||||
|
||||
expect(index.allCountableNotesCount()).toEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { isTag, SNTag } from '../../../Syncable/Tag/Tag'
|
||||
import { SNIndex } from '../../Index/SNIndex'
|
||||
import { ItemCollection } from './ItemCollection'
|
||||
import { ItemDelta } from '../../Index/ItemDelta'
|
||||
import { isDecryptedItem, ItemInterface } from '../../../Abstract/Item'
|
||||
|
||||
type AllNotesUuidSignifier = undefined
|
||||
export type TagNoteCountChangeObserver = (tagUuid: Uuid | AllNotesUuidSignifier) => void
|
||||
|
||||
export class TagNotesIndex implements SNIndex {
|
||||
private tagToNotesMap: Partial<Record<Uuid, Set<Uuid>>> = {}
|
||||
private allCountableNotes = new Set<Uuid>()
|
||||
|
||||
constructor(private collection: ItemCollection, public observers: TagNoteCountChangeObserver[] = []) {}
|
||||
|
||||
private isNoteCountable = (note: ItemInterface) => {
|
||||
if (isDecryptedItem(note)) {
|
||||
return !note.archived && !note.trashed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public addCountChangeObserver(observer: TagNoteCountChangeObserver): () => void {
|
||||
this.observers.push(observer)
|
||||
|
||||
const thislessEventObservers = this.observers
|
||||
return () => {
|
||||
removeFromArray(thislessEventObservers, observer)
|
||||
}
|
||||
}
|
||||
|
||||
private notifyObservers(tagUuid: Uuid | undefined) {
|
||||
for (const observer of this.observers) {
|
||||
observer(tagUuid)
|
||||
}
|
||||
}
|
||||
|
||||
public allCountableNotesCount(): number {
|
||||
return this.allCountableNotes.size
|
||||
}
|
||||
|
||||
public countableNotesForTag(tag: SNTag): number {
|
||||
return this.tagToNotesMap[tag.uuid]?.size || 0
|
||||
}
|
||||
|
||||
public onChange(delta: ItemDelta): void {
|
||||
const notes = [...delta.changed, ...delta.inserted, ...delta.discarded].filter(
|
||||
(i) => i.content_type === ContentType.Note,
|
||||
)
|
||||
const tags = [...delta.changed, ...delta.inserted].filter(isDecryptedItem).filter(isTag)
|
||||
|
||||
this.receiveNoteChanges(notes)
|
||||
this.receiveTagChanges(tags)
|
||||
}
|
||||
|
||||
private receiveTagChanges(tags: SNTag[]): void {
|
||||
for (const tag of tags) {
|
||||
const uuids = tag.noteReferences.map((ref) => ref.uuid)
|
||||
const countableUuids = uuids.filter((uuid) => this.allCountableNotes.has(uuid))
|
||||
const previousSet = this.tagToNotesMap[tag.uuid]
|
||||
this.tagToNotesMap[tag.uuid] = new Set(countableUuids)
|
||||
|
||||
if (previousSet?.size !== countableUuids.length) {
|
||||
this.notifyObservers(tag.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private receiveNoteChanges(notes: ItemInterface[]): void {
|
||||
const previousAllCount = this.allCountableNotes.size
|
||||
|
||||
for (const note of notes) {
|
||||
const isCountable = this.isNoteCountable(note)
|
||||
if (isCountable) {
|
||||
this.allCountableNotes.add(note.uuid)
|
||||
} else {
|
||||
this.allCountableNotes.delete(note.uuid)
|
||||
}
|
||||
|
||||
const associatedTagUuids = this.collection.uuidsThatReferenceUuid(note.uuid)
|
||||
|
||||
for (const tagUuid of associatedTagUuids) {
|
||||
const set = this.setForTag(tagUuid)
|
||||
const previousCount = set.size
|
||||
if (isCountable) {
|
||||
set.add(note.uuid)
|
||||
} else {
|
||||
set.delete(note.uuid)
|
||||
}
|
||||
if (previousCount !== set.size) {
|
||||
this.notifyObservers(tagUuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (previousAllCount !== this.allCountableNotes.size) {
|
||||
this.notifyObservers(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
private setForTag(uuid: Uuid): Set<Uuid> {
|
||||
let set = this.tagToNotesMap[uuid]
|
||||
if (!set) {
|
||||
set = new Set()
|
||||
this.tagToNotesMap[uuid] = set
|
||||
}
|
||||
return set
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { FullyFormedPayloadInterface } from './../../../Abstract/Payload/Interfaces/UnionTypes'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { UuidMap } from '@standardnotes/utils'
|
||||
import { PayloadCollection } from './PayloadCollection'
|
||||
|
||||
export class ImmutablePayloadCollection<
|
||||
P extends FullyFormedPayloadInterface = FullyFormedPayloadInterface,
|
||||
> extends PayloadCollection<P> {
|
||||
public get payloads(): P[] {
|
||||
return this.all()
|
||||
}
|
||||
|
||||
/** We don't use a constructor for this because we don't want the constructor to have
|
||||
* side-effects, such as calling collection.set(). */
|
||||
static WithPayloads<T extends FullyFormedPayloadInterface>(payloads: T[] = []): ImmutablePayloadCollection<T> {
|
||||
const collection = new ImmutablePayloadCollection<T>()
|
||||
if (payloads.length > 0) {
|
||||
collection.set(payloads)
|
||||
}
|
||||
|
||||
Object.freeze(collection)
|
||||
return collection
|
||||
}
|
||||
|
||||
static FromCollection<T extends FullyFormedPayloadInterface>(
|
||||
collection: PayloadCollection<T>,
|
||||
): ImmutablePayloadCollection<T> {
|
||||
const mapCopy = Object.freeze(Object.assign({}, collection.map))
|
||||
const typedMapCopy = Object.freeze(Object.assign({}, collection.typedMap))
|
||||
const referenceMapCopy = Object.freeze(collection.referenceMap.makeCopy()) as UuidMap
|
||||
const conflictMapCopy = Object.freeze(collection.conflictMap.makeCopy()) as UuidMap
|
||||
|
||||
const result = new ImmutablePayloadCollection<T>(
|
||||
true,
|
||||
mapCopy,
|
||||
typedMapCopy as Partial<Record<ContentType, T[]>>,
|
||||
referenceMapCopy,
|
||||
conflictMapCopy,
|
||||
)
|
||||
|
||||
Object.freeze(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
mutableCopy(): PayloadCollection<P> {
|
||||
const mapCopy = Object.assign({}, this.map)
|
||||
const typedMapCopy = Object.assign({}, this.typedMap)
|
||||
const referenceMapCopy = this.referenceMap.makeCopy()
|
||||
const conflictMapCopy = this.conflictMap.makeCopy()
|
||||
const result = new PayloadCollection(true, mapCopy, typedMapCopy, referenceMapCopy, conflictMapCopy)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { FullyFormedPayloadInterface } from './../../../Abstract/Payload/Interfaces/UnionTypes'
|
||||
import { EncryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/EncryptedPayload'
|
||||
import { CollectionInterface } from '../CollectionInterface'
|
||||
import { DecryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { IntegrityPayload } from '@standardnotes/responses'
|
||||
import { Collection } from '../Collection'
|
||||
import { DeletedPayloadInterface } from '../../../Abstract/Payload'
|
||||
|
||||
export class PayloadCollection<P extends FullyFormedPayloadInterface = FullyFormedPayloadInterface>
|
||||
extends Collection<P, DecryptedPayloadInterface, EncryptedPayloadInterface, DeletedPayloadInterface>
|
||||
implements CollectionInterface
|
||||
{
|
||||
public integrityPayloads(): IntegrityPayload[] {
|
||||
const nondeletedElements = this.nondeletedElements()
|
||||
|
||||
return nondeletedElements.map((item) => ({
|
||||
uuid: item.uuid,
|
||||
updated_at_timestamp: item.serverUpdatedAtTimestamp as number,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { extendArray } from '@standardnotes/utils'
|
||||
import { EncryptedPayloadInterface, FullyFormedPayloadInterface, PayloadEmitSource } from '../../../Abstract/Payload'
|
||||
import { SyncResolvedPayload } from '../Utilities/SyncResolvedPayload'
|
||||
|
||||
export type DeltaEmit<P extends FullyFormedPayloadInterface = FullyFormedPayloadInterface> = {
|
||||
emits: P[]
|
||||
ignored?: EncryptedPayloadInterface[]
|
||||
source: PayloadEmitSource
|
||||
}
|
||||
|
||||
export type SyncDeltaEmit = {
|
||||
emits: SyncResolvedPayload[]
|
||||
ignored?: EncryptedPayloadInterface[]
|
||||
source: PayloadEmitSource
|
||||
}
|
||||
|
||||
export type SourcelessSyncDeltaEmit = {
|
||||
emits: SyncResolvedPayload[]
|
||||
ignored: EncryptedPayloadInterface[]
|
||||
}
|
||||
|
||||
export function extendSyncDelta(base: SyncDeltaEmit, extendWith: SourcelessSyncDeltaEmit): void {
|
||||
extendArray(base.emits, extendWith.emits)
|
||||
if (extendWith.ignored) {
|
||||
if (!base.ignored) {
|
||||
base.ignored = []
|
||||
}
|
||||
extendArray(base.ignored, extendWith.ignored)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ImmutablePayloadCollection } from '../../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { DeltaEmit } from './DeltaEmit'
|
||||
|
||||
/**
|
||||
* A payload delta is a class that defines instructions that process an incoming collection
|
||||
* of payloads, applies some set of operations on those payloads wrt to the current base state,
|
||||
* and returns the resulting collection. Deltas are purely functional and do not modify
|
||||
* input data, instead returning what the collection would look like after its been
|
||||
* transformed. The consumer may choose to act as they wish with this end result.
|
||||
*
|
||||
* A delta object takes a baseCollection (the current state of the data) and an applyCollection
|
||||
* (the data another source is attempting to merge on top of our base data). The delta will
|
||||
* then iterate over this data and return a `resultingCollection` object that includes the final
|
||||
* state of the data after the class-specific operations have been applied.
|
||||
*
|
||||
* For example, the RemoteRetrieved delta will take the current state of local data as
|
||||
* baseCollection, the data the server is sending as applyCollection, and determine what
|
||||
* the end state of the data should look like.
|
||||
*/
|
||||
export interface DeltaInterface {
|
||||
baseCollection: ImmutablePayloadCollection
|
||||
|
||||
result(): DeltaEmit
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ImmutablePayloadCollection } from '../../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { SyncDeltaEmit } from './DeltaEmit'
|
||||
|
||||
export interface SyncDeltaInterface {
|
||||
baseCollection: ImmutablePayloadCollection
|
||||
|
||||
result(): SyncDeltaEmit
|
||||
}
|
||||
102
packages/models/src/Domain/Runtime/Deltas/Conflict.spec.ts
Normal file
102
packages/models/src/Domain/Runtime/Deltas/Conflict.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { ConflictStrategy } from '../../Abstract/Item'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
EncryptedPayload,
|
||||
FullyFormedPayloadInterface,
|
||||
PayloadTimestampDefaults,
|
||||
} from '../../Abstract/Payload'
|
||||
import { ItemsKeyContent } from '../../Syncable/ItemsKey/ItemsKeyInterface'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { PayloadCollection } from '../Collection/Payload/PayloadCollection'
|
||||
import { HistoryMap } from '../History'
|
||||
import { ConflictDelta } from './Conflict'
|
||||
|
||||
describe('conflict delta', () => {
|
||||
const historyMap = {} as HistoryMap
|
||||
|
||||
const createBaseCollection = (payload: FullyFormedPayloadInterface) => {
|
||||
const baseCollection = new PayloadCollection()
|
||||
baseCollection.set(payload)
|
||||
return ImmutablePayloadCollection.FromCollection(baseCollection)
|
||||
}
|
||||
|
||||
const createDecryptedItemsKey = (uuid: string, key: string, timestamp = 0) => {
|
||||
return new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: uuid,
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: key,
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
updated_at_timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
const createErroredItemsKey = (uuid: string, timestamp = 0) => {
|
||||
return new EncryptedPayload({
|
||||
uuid: uuid,
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: '004:...',
|
||||
enc_item_key: '004:...',
|
||||
items_key_id: undefined,
|
||||
errorDecrypting: true,
|
||||
waitingForKey: false,
|
||||
...PayloadTimestampDefaults(),
|
||||
updated_at_timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
it('when apply is an items key, logic should be diverted to items key delta', () => {
|
||||
const basePayload = createDecryptedItemsKey('123', 'secret')
|
||||
|
||||
const baseCollection = createBaseCollection(basePayload)
|
||||
|
||||
const applyPayload = createDecryptedItemsKey('123', 'secret', 2)
|
||||
|
||||
const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap)
|
||||
|
||||
const mocked = (delta.getConflictStrategy = jest.fn())
|
||||
|
||||
delta.result()
|
||||
|
||||
expect(mocked).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it('if apply payload is errored but base payload is not, should duplicate base and keep apply', () => {
|
||||
const basePayload = createDecryptedItemsKey('123', 'secret')
|
||||
|
||||
const baseCollection = createBaseCollection(basePayload)
|
||||
|
||||
const applyPayload = createErroredItemsKey('123', 2)
|
||||
|
||||
const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap)
|
||||
|
||||
expect(delta.getConflictStrategy()).toBe(ConflictStrategy.DuplicateBaseKeepApply)
|
||||
})
|
||||
|
||||
it('if base payload is errored but apply is not, should keep base duplicate apply', () => {
|
||||
const basePayload = createErroredItemsKey('123', 2)
|
||||
|
||||
const baseCollection = createBaseCollection(basePayload)
|
||||
|
||||
const applyPayload = createDecryptedItemsKey('123', 'secret')
|
||||
|
||||
const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap)
|
||||
|
||||
expect(delta.getConflictStrategy()).toBe(ConflictStrategy.KeepBaseDuplicateApply)
|
||||
})
|
||||
|
||||
it('if base and apply are errored, should keep apply', () => {
|
||||
const basePayload = createErroredItemsKey('123', 2)
|
||||
|
||||
const baseCollection = createBaseCollection(basePayload)
|
||||
|
||||
const applyPayload = createErroredItemsKey('123', 3)
|
||||
|
||||
const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap)
|
||||
|
||||
expect(delta.getConflictStrategy()).toBe(ConflictStrategy.KeepApply)
|
||||
})
|
||||
})
|
||||
225
packages/models/src/Domain/Runtime/Deltas/Conflict.ts
Normal file
225
packages/models/src/Domain/Runtime/Deltas/Conflict.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { greaterOfTwoDates, uniqCombineObjArrays } from '@standardnotes/utils'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { CreateDecryptedItemFromPayload, CreateItemFromPayload } from '../../Utilities/Item/ItemGenerator'
|
||||
import { HistoryMap, historyMapFunctions } from '../History/HistoryMap'
|
||||
import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy'
|
||||
import { PayloadsByDuplicating } from '../../Utilities/Payload/PayloadsByDuplicating'
|
||||
import { PayloadContentsEqual } from '../../Utilities/Payload/PayloadContentsEqual'
|
||||
import { FullyFormedPayloadInterface } from '../../Abstract/Payload'
|
||||
import {
|
||||
isDecryptedPayload,
|
||||
isErrorDecryptingPayload,
|
||||
isDeletedPayload,
|
||||
} from '../../Abstract/Payload/Interfaces/TypeCheck'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
import { ItemsKeyDelta } from './ItemsKeyDelta'
|
||||
import { SourcelessSyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter'
|
||||
|
||||
export class ConflictDelta {
|
||||
constructor(
|
||||
protected readonly baseCollection: ImmutablePayloadCollection,
|
||||
protected readonly basePayload: FullyFormedPayloadInterface,
|
||||
protected readonly applyPayload: FullyFormedPayloadInterface,
|
||||
protected readonly historyMap: HistoryMap,
|
||||
) {}
|
||||
|
||||
public result(): SourcelessSyncDeltaEmit {
|
||||
if (this.applyPayload.content_type === ContentType.ItemsKey) {
|
||||
const keyDelta = new ItemsKeyDelta(this.baseCollection, [this.applyPayload])
|
||||
|
||||
return keyDelta.result()
|
||||
}
|
||||
|
||||
const strategy = this.getConflictStrategy()
|
||||
|
||||
return {
|
||||
emits: this.handleStrategy(strategy),
|
||||
ignored: [],
|
||||
}
|
||||
}
|
||||
|
||||
getConflictStrategy(): ConflictStrategy {
|
||||
const isBaseErrored = isErrorDecryptingPayload(this.basePayload)
|
||||
const isApplyErrored = isErrorDecryptingPayload(this.applyPayload)
|
||||
if (isBaseErrored || isApplyErrored) {
|
||||
if (isBaseErrored && !isApplyErrored) {
|
||||
return ConflictStrategy.KeepBaseDuplicateApply
|
||||
} else if (!isBaseErrored && isApplyErrored) {
|
||||
return ConflictStrategy.DuplicateBaseKeepApply
|
||||
} else if (isBaseErrored && isApplyErrored) {
|
||||
return ConflictStrategy.KeepApply
|
||||
}
|
||||
} else if (isDecryptedPayload(this.basePayload)) {
|
||||
/**
|
||||
* Ensure no conflict has already been created with the incoming content.
|
||||
* This can occur in a multi-page sync request where in the middle of the request,
|
||||
* we make changes to many items, including duplicating, but since we are still not
|
||||
* uploading the changes until after the multi-page request completes, we may have
|
||||
* already conflicted this item.
|
||||
*/
|
||||
const existingConflict = this.baseCollection.conflictsOf(this.applyPayload.uuid)[0]
|
||||
if (
|
||||
existingConflict &&
|
||||
isDecryptedPayload(existingConflict) &&
|
||||
isDecryptedPayload(this.applyPayload) &&
|
||||
PayloadContentsEqual(existingConflict, this.applyPayload)
|
||||
) {
|
||||
/** Conflict exists and its contents are the same as incoming value, do not make duplicate */
|
||||
return ConflictStrategy.KeepBase
|
||||
} else {
|
||||
const tmpBaseItem = CreateDecryptedItemFromPayload(this.basePayload)
|
||||
const tmpApplyItem = CreateItemFromPayload(this.applyPayload)
|
||||
const historyEntries = this.historyMap[this.basePayload.uuid] || []
|
||||
const previousRevision = historyMapFunctions.getNewestRevision(historyEntries)
|
||||
|
||||
return tmpBaseItem.strategyWhenConflictingWithItem(tmpApplyItem, previousRevision)
|
||||
}
|
||||
} else if (isDeletedPayload(this.basePayload) || isDeletedPayload(this.applyPayload)) {
|
||||
const baseDeleted = isDeletedPayload(this.basePayload)
|
||||
const applyDeleted = isDeletedPayload(this.applyPayload)
|
||||
if (baseDeleted && applyDeleted) {
|
||||
return ConflictStrategy.KeepApply
|
||||
} else {
|
||||
return ConflictStrategy.KeepApply
|
||||
}
|
||||
}
|
||||
|
||||
throw Error('Unhandled strategy in Conflict Delta getConflictStrategy')
|
||||
}
|
||||
|
||||
private handleStrategy(strategy: ConflictStrategy): SyncResolvedPayload[] {
|
||||
if (strategy === ConflictStrategy.KeepBase) {
|
||||
return this.handleKeepBaseStrategy()
|
||||
}
|
||||
|
||||
if (strategy === ConflictStrategy.KeepApply) {
|
||||
return this.handleKeepApplyStrategy()
|
||||
}
|
||||
|
||||
if (strategy === ConflictStrategy.KeepBaseDuplicateApply) {
|
||||
return this.handleKeepBaseDuplicateApplyStrategy()
|
||||
}
|
||||
|
||||
if (strategy === ConflictStrategy.DuplicateBaseKeepApply) {
|
||||
return this.handleDuplicateBaseKeepApply()
|
||||
}
|
||||
|
||||
if (strategy === ConflictStrategy.KeepBaseMergeRefs) {
|
||||
return this.handleKeepBaseMergeRefsStrategy()
|
||||
}
|
||||
|
||||
throw Error('Unhandled strategy in conflict delta payloadsByHandlingStrategy')
|
||||
}
|
||||
|
||||
private handleKeepBaseStrategy(): SyncResolvedPayload[] {
|
||||
const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt)
|
||||
|
||||
const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp)
|
||||
|
||||
const leftPayload = this.basePayload.copyAsSyncResolved(
|
||||
{
|
||||
updated_at: updatedAt,
|
||||
updated_at_timestamp: updatedAtTimestamp,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
dirty: true,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
this.applyPayload.source,
|
||||
)
|
||||
|
||||
return [leftPayload]
|
||||
}
|
||||
|
||||
private handleKeepApplyStrategy(): SyncResolvedPayload[] {
|
||||
const result = this.applyPayload.copyAsSyncResolved(
|
||||
{
|
||||
lastSyncBegan: this.basePayload.lastSyncBegan,
|
||||
lastSyncEnd: new Date(),
|
||||
dirty: false,
|
||||
},
|
||||
this.applyPayload.source,
|
||||
)
|
||||
|
||||
return [result]
|
||||
}
|
||||
|
||||
private handleKeepBaseDuplicateApplyStrategy(): SyncResolvedPayload[] {
|
||||
const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt)
|
||||
|
||||
const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp)
|
||||
|
||||
const leftPayload = this.basePayload.copyAsSyncResolved(
|
||||
{
|
||||
updated_at: updatedAt,
|
||||
updated_at_timestamp: updatedAtTimestamp,
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
this.applyPayload.source,
|
||||
)
|
||||
|
||||
const rightPayloads = PayloadsByDuplicating({
|
||||
payload: this.applyPayload,
|
||||
baseCollection: this.baseCollection,
|
||||
isConflict: true,
|
||||
source: this.applyPayload.source,
|
||||
})
|
||||
|
||||
return [leftPayload].concat(rightPayloads)
|
||||
}
|
||||
|
||||
private handleDuplicateBaseKeepApply(): SyncResolvedPayload[] {
|
||||
const leftPayloads = PayloadsByDuplicating({
|
||||
payload: this.basePayload,
|
||||
baseCollection: this.baseCollection,
|
||||
isConflict: true,
|
||||
source: this.applyPayload.source,
|
||||
})
|
||||
|
||||
const rightPayload = this.applyPayload.copyAsSyncResolved(
|
||||
{
|
||||
lastSyncBegan: this.basePayload.lastSyncBegan,
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
this.applyPayload.source,
|
||||
)
|
||||
|
||||
return leftPayloads.concat([rightPayload])
|
||||
}
|
||||
|
||||
private handleKeepBaseMergeRefsStrategy(): SyncResolvedPayload[] {
|
||||
if (!isDecryptedPayload(this.basePayload) || !isDecryptedPayload(this.applyPayload)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const refs = uniqCombineObjArrays(this.basePayload.content.references, this.applyPayload.content.references, [
|
||||
'uuid',
|
||||
'content_type',
|
||||
])
|
||||
|
||||
const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt)
|
||||
|
||||
const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp)
|
||||
|
||||
const payload = this.basePayload.copyAsSyncResolved(
|
||||
{
|
||||
updated_at: updatedAt,
|
||||
updated_at_timestamp: updatedAtTimestamp,
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
lastSyncEnd: new Date(),
|
||||
content: {
|
||||
...this.basePayload.content,
|
||||
references: refs,
|
||||
},
|
||||
},
|
||||
this.applyPayload.source,
|
||||
)
|
||||
|
||||
return [payload]
|
||||
}
|
||||
}
|
||||
90
packages/models/src/Domain/Runtime/Deltas/FileImport.ts
Normal file
90
packages/models/src/Domain/Runtime/Deltas/FileImport.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { ConflictDelta } from './Conflict'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { DeletedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { HistoryMap } from '../History'
|
||||
import { extendSyncDelta, SourcelessSyncDeltaEmit, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { DeltaInterface } from './Abstract/DeltaInterface'
|
||||
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter'
|
||||
|
||||
export class DeltaFileImport implements DeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
private readonly applyPayloads: DecryptedPayloadInterface[],
|
||||
protected readonly historyMap: HistoryMap,
|
||||
) {}
|
||||
|
||||
public result(): SyncDeltaEmit {
|
||||
const result: SyncDeltaEmit = {
|
||||
emits: [],
|
||||
ignored: [],
|
||||
source: PayloadEmitSource.FileImport,
|
||||
}
|
||||
|
||||
for (const payload of this.applyPayloads) {
|
||||
const resolved = this.resolvePayload(payload, result)
|
||||
|
||||
extendSyncDelta(result, resolved)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private resolvePayload(
|
||||
payload: DecryptedPayloadInterface | DeletedPayloadInterface,
|
||||
currentResults: SyncDeltaEmit,
|
||||
): SourcelessSyncDeltaEmit {
|
||||
/**
|
||||
* Check to see if we've already processed a payload for this id.
|
||||
* If so, that would be the latest value, and not what's in the base collection.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Find the most recently created conflict if available, as that
|
||||
* would contain the most recent value.
|
||||
*/
|
||||
let current = currentResults.emits.find((candidate) => {
|
||||
return isDecryptedPayload(candidate) && candidate.content.conflict_of === payload.uuid
|
||||
})
|
||||
|
||||
/**
|
||||
* If no latest conflict, find by uuid directly.
|
||||
*/
|
||||
if (!current) {
|
||||
current = currentResults.emits.find((candidate) => {
|
||||
return candidate.uuid === payload.uuid
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* If not found in current results, use the base value.
|
||||
*/
|
||||
if (!current) {
|
||||
const base = this.baseCollection.find(payload.uuid)
|
||||
if (base && isDecryptedPayload(base)) {
|
||||
current = base as SyncResolvedPayload
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the current doesn't exist, we're creating a new item from payload.
|
||||
*/
|
||||
if (!current) {
|
||||
return {
|
||||
emits: [
|
||||
payload.copyAsSyncResolved({
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
lastSyncEnd: new Date(0),
|
||||
}),
|
||||
],
|
||||
ignored: [],
|
||||
}
|
||||
}
|
||||
|
||||
const delta = new ConflictDelta(this.baseCollection, current, payload, this.historyMap)
|
||||
|
||||
return delta.result()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
EncryptedPayload,
|
||||
isEncryptedPayload,
|
||||
PayloadTimestampDefaults,
|
||||
} from '../../Abstract/Payload'
|
||||
import { PayloadCollection } from '../Collection/Payload/PayloadCollection'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { ItemsKeyContent } from '../../Syncable/ItemsKey/ItemsKeyInterface'
|
||||
import { ItemsKeyDelta } from './ItemsKeyDelta'
|
||||
|
||||
describe('items key delta', () => {
|
||||
it('if local items key is decrypted, incoming encrypted should not overwrite', async () => {
|
||||
const baseCollection = new PayloadCollection()
|
||||
const basePayload = new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: '123',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: 'secret',
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
updated_at_timestamp: 1,
|
||||
})
|
||||
|
||||
baseCollection.set(basePayload)
|
||||
|
||||
const payloadToIgnore = new EncryptedPayload({
|
||||
uuid: '123',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: '004:...',
|
||||
enc_item_key: '004:...',
|
||||
items_key_id: undefined,
|
||||
errorDecrypting: false,
|
||||
waitingForKey: false,
|
||||
...PayloadTimestampDefaults(),
|
||||
updated_at_timestamp: 2,
|
||||
})
|
||||
|
||||
const delta = new ItemsKeyDelta(ImmutablePayloadCollection.FromCollection(baseCollection), [payloadToIgnore])
|
||||
|
||||
const result = delta.result()
|
||||
const updatedBasePayload = result.emits?.[0] as DecryptedPayload<ItemsKeyContent>
|
||||
|
||||
expect(updatedBasePayload.content.itemsKey).toBe('secret')
|
||||
expect(updatedBasePayload.updated_at_timestamp).toBe(2)
|
||||
expect(updatedBasePayload.dirty).toBeFalsy()
|
||||
|
||||
const ignored = result.ignored?.[0] as EncryptedPayload
|
||||
expect(ignored).toBeTruthy()
|
||||
expect(isEncryptedPayload(ignored)).toBe(true)
|
||||
})
|
||||
})
|
||||
52
packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.ts
Normal file
52
packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import {
|
||||
EncryptedPayloadInterface,
|
||||
FullyFormedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isEncryptedPayload,
|
||||
} from '../../Abstract/Payload'
|
||||
import { SourcelessSyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState'
|
||||
|
||||
export class ItemsKeyDelta {
|
||||
constructor(
|
||||
private baseCollection: ImmutablePayloadCollection,
|
||||
private readonly applyPayloads: FullyFormedPayloadInterface[],
|
||||
) {}
|
||||
|
||||
public result(): SourcelessSyncDeltaEmit {
|
||||
const emits: SyncResolvedPayload[] = []
|
||||
const ignored: EncryptedPayloadInterface[] = []
|
||||
|
||||
for (const apply of this.applyPayloads) {
|
||||
const base = this.baseCollection.find(apply.uuid)
|
||||
|
||||
if (!base) {
|
||||
emits.push(payloadByFinalizingSyncState(apply, this.baseCollection))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (isEncryptedPayload(apply) && isDecryptedPayload(base)) {
|
||||
const keepBaseWithApplyTimestamps = base.copyAsSyncResolved({
|
||||
updated_at_timestamp: apply.updated_at_timestamp,
|
||||
updated_at: apply.updated_at,
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
})
|
||||
|
||||
emits.push(keepBaseWithApplyTimestamps)
|
||||
|
||||
ignored.push(apply)
|
||||
} else {
|
||||
emits.push(payloadByFinalizingSyncState(apply, this.baseCollection))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
emits: emits,
|
||||
ignored,
|
||||
}
|
||||
}
|
||||
}
|
||||
33
packages/models/src/Domain/Runtime/Deltas/OfflineSaved.ts
Normal file
33
packages/models/src/Domain/Runtime/Deltas/OfflineSaved.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { OfflineSyncSavedContextualPayload } from '../../Abstract/Contextual/OfflineSyncSaved'
|
||||
import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState'
|
||||
import { SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
|
||||
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
|
||||
export class DeltaOfflineSaved implements SyncDeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection<FullyFormedPayloadInterface>,
|
||||
readonly applyContextualPayloads: OfflineSyncSavedContextualPayload[],
|
||||
) {}
|
||||
|
||||
public result(): SyncDeltaEmit {
|
||||
const processed: SyncResolvedPayload[] = []
|
||||
|
||||
for (const apply of this.applyContextualPayloads) {
|
||||
const base = this.baseCollection.find(apply.uuid)
|
||||
|
||||
if (!base) {
|
||||
continue
|
||||
}
|
||||
|
||||
processed.push(payloadByFinalizingSyncState(base, this.baseCollection))
|
||||
}
|
||||
|
||||
return {
|
||||
emits: processed,
|
||||
source: PayloadEmitSource.OfflineSyncSaved,
|
||||
}
|
||||
}
|
||||
}
|
||||
62
packages/models/src/Domain/Runtime/Deltas/OutOfSync.ts
Normal file
62
packages/models/src/Domain/Runtime/Deltas/OutOfSync.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck'
|
||||
import { PayloadContentsEqual } from '../../Utilities/Payload/PayloadContentsEqual'
|
||||
import { ConflictDelta } from './Conflict'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ItemsKeyDelta } from './ItemsKeyDelta'
|
||||
import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { HistoryMap } from '../History'
|
||||
import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
|
||||
|
||||
export class DeltaOutOfSync implements SyncDeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
readonly applyCollection: ImmutablePayloadCollection,
|
||||
readonly historyMap: HistoryMap,
|
||||
) {}
|
||||
|
||||
public result(): SyncDeltaEmit {
|
||||
const result: SyncDeltaEmit = {
|
||||
emits: [],
|
||||
ignored: [],
|
||||
source: PayloadEmitSource.RemoteRetrieved,
|
||||
}
|
||||
|
||||
for (const apply of this.applyCollection.all()) {
|
||||
if (apply.content_type === ContentType.ItemsKey) {
|
||||
const itemsKeyDeltaEmit = new ItemsKeyDelta(this.baseCollection, [apply]).result()
|
||||
|
||||
extendSyncDelta(result, itemsKeyDeltaEmit)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const base = this.baseCollection.find(apply.uuid)
|
||||
|
||||
if (!base) {
|
||||
result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const isBaseDecrypted = isDecryptedPayload(base)
|
||||
const isApplyDecrypted = isDecryptedPayload(apply)
|
||||
|
||||
const needsConflict =
|
||||
isApplyDecrypted !== isBaseDecrypted ||
|
||||
(isApplyDecrypted && isBaseDecrypted && !PayloadContentsEqual(apply, base))
|
||||
|
||||
if (needsConflict) {
|
||||
const delta = new ConflictDelta(this.baseCollection, base, apply, this.historyMap)
|
||||
|
||||
extendSyncDelta(result, delta.result())
|
||||
} else {
|
||||
result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ConflictDelta } from './Conflict'
|
||||
import { PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { HistoryMap } from '../History'
|
||||
import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
|
||||
import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState'
|
||||
|
||||
export class DeltaRemoteDataConflicts implements SyncDeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
readonly applyCollection: ImmutablePayloadCollection,
|
||||
readonly historyMap: HistoryMap,
|
||||
) {}
|
||||
|
||||
public result(): SyncDeltaEmit {
|
||||
const result: SyncDeltaEmit = {
|
||||
emits: [],
|
||||
ignored: [],
|
||||
source: PayloadEmitSource.RemoteRetrieved,
|
||||
}
|
||||
|
||||
for (const apply of this.applyCollection.all()) {
|
||||
const base = this.baseCollection.find(apply.uuid)
|
||||
|
||||
const isBaseDeleted = base == undefined
|
||||
|
||||
if (isBaseDeleted) {
|
||||
result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const delta = new ConflictDelta(this.baseCollection, base, apply, this.historyMap)
|
||||
|
||||
extendSyncDelta(result, delta.result())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||
import { NoteContent } from '../../Syncable/Note'
|
||||
import { PayloadCollection } from '../Collection/Payload/PayloadCollection'
|
||||
import { DeltaRemoteRejected } from './RemoteRejected'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
|
||||
describe('remote rejected delta', () => {
|
||||
it('rejected payloads should not map onto app state', async () => {
|
||||
const baseCollection = new PayloadCollection()
|
||||
const basePayload = new DecryptedPayload<NoteContent>({
|
||||
uuid: '123',
|
||||
content_type: ContentType.Note,
|
||||
dirty: true,
|
||||
content: FillItemContent<NoteContent>({
|
||||
title: 'foo',
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
updated_at_timestamp: 1,
|
||||
})
|
||||
|
||||
baseCollection.set(basePayload)
|
||||
|
||||
const rejectedPayload = basePayload.copy({
|
||||
content: FillItemContent<NoteContent>({
|
||||
title: 'rejected',
|
||||
}),
|
||||
updated_at_timestamp: 3,
|
||||
dirty: true,
|
||||
})
|
||||
|
||||
const delta = new DeltaRemoteRejected(
|
||||
ImmutablePayloadCollection.FromCollection(baseCollection),
|
||||
ImmutablePayloadCollection.WithPayloads([rejectedPayload]),
|
||||
)
|
||||
|
||||
const result = delta.result()
|
||||
const payload = result.emits[0] as DecryptedPayload<NoteContent>
|
||||
|
||||
expect(payload.content.title).toBe('foo')
|
||||
expect(payload.updated_at_timestamp).toBe(1)
|
||||
expect(payload.dirty).toBeFalsy()
|
||||
})
|
||||
})
|
||||
40
packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts
Normal file
40
packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource'
|
||||
import { PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
|
||||
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
|
||||
|
||||
export class DeltaRemoteRejected implements SyncDeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
readonly applyCollection: ImmutablePayloadCollection,
|
||||
) {}
|
||||
|
||||
public result(): SyncDeltaEmit {
|
||||
const results: SyncResolvedPayload[] = []
|
||||
|
||||
for (const apply of this.applyCollection.all()) {
|
||||
const base = this.baseCollection.find(apply.uuid)
|
||||
|
||||
if (!base) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = base.copyAsSyncResolved(
|
||||
{
|
||||
dirty: false,
|
||||
lastSyncEnd: new Date(),
|
||||
},
|
||||
PayloadSource.RemoteSaved,
|
||||
)
|
||||
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
return {
|
||||
emits: results,
|
||||
source: PayloadEmitSource.RemoteSaved,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
EncryptedPayload,
|
||||
isEncryptedPayload,
|
||||
PayloadTimestampDefaults,
|
||||
} from '../../Abstract/Payload'
|
||||
import { PayloadCollection } from '../Collection/Payload/PayloadCollection'
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { ItemsKeyContent } from '../../Syncable/ItemsKey/ItemsKeyInterface'
|
||||
import { DeltaRemoteRetrieved } from './RemoteRetrieved'
|
||||
|
||||
describe('remote retrieved delta', () => {
|
||||
it('if local items key is decrypted, incoming encrypted should not overwrite', async () => {
|
||||
const baseCollection = new PayloadCollection()
|
||||
const basePayload = new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: '123',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: 'secret',
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
updated_at_timestamp: 1,
|
||||
})
|
||||
|
||||
baseCollection.set(basePayload)
|
||||
|
||||
const payloadToIgnore = new EncryptedPayload({
|
||||
uuid: '123',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: '004:...',
|
||||
enc_item_key: '004:...',
|
||||
items_key_id: undefined,
|
||||
errorDecrypting: false,
|
||||
waitingForKey: false,
|
||||
...PayloadTimestampDefaults(),
|
||||
updated_at_timestamp: 2,
|
||||
})
|
||||
|
||||
const delta = new DeltaRemoteRetrieved(
|
||||
ImmutablePayloadCollection.FromCollection(baseCollection),
|
||||
ImmutablePayloadCollection.WithPayloads([payloadToIgnore]),
|
||||
[],
|
||||
{},
|
||||
)
|
||||
|
||||
const result = delta.result()
|
||||
|
||||
const updatedBasePayload = result.emits?.[0] as DecryptedPayload<ItemsKeyContent>
|
||||
|
||||
expect(updatedBasePayload.content.itemsKey).toBe('secret')
|
||||
expect(updatedBasePayload.updated_at_timestamp).toBe(2)
|
||||
expect(updatedBasePayload.dirty).toBeFalsy()
|
||||
|
||||
const ignored = result.ignored?.[0] as EncryptedPayload
|
||||
expect(ignored).toBeTruthy()
|
||||
expect(isEncryptedPayload(ignored)).toBe(true)
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user