浏览代码

feat: domain items (#655)

* feat: content type as a value object

* feat: turn items into domain entities

* fix: update @standardnotes/api

* fix(syncing-server): bindings order
Karol Sójko 1 年之前
父节点
当前提交
a0af8f0025
共有 100 个文件被更改,包括 2188 次插入1785 次删除
  1. 58 32
      .pnp.cjs
  2. 二进制
      .yarn/cache/@standardnotes-api-npm-1.26.10-f6165cafd3-3c3561aec8.zip
  3. 二进制
      .yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip
  4. 二进制
      .yarn/cache/@standardnotes-encryption-npm-1.21.38-d08c3d4766-1393840523.zip
  5. 二进制
      .yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip
  6. 二进制
      .yarn/cache/@standardnotes-models-npm-1.45.5-29326e959c-15f26c11b2.zip
  7. 二进制
      .yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip
  8. 二进制
      .yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip
  9. 二进制
      .yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip
  10. 二进制
      .yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip
  11. 1 1
      packages/auth/package.json
  12. 4 4
      packages/auth/src/Controller/AuthController.spec.ts
  13. 9 3
      packages/auth/src/Controller/AuthController.ts
  14. 11 11
      packages/auth/src/Controller/SubscriptionInvitesController.spec.ts
  15. 2 2
      packages/auth/src/Domain/UseCase/Register.ts
  16. 2 2
      packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts
  17. 0 48
      packages/common/src/Domain/Content/ContentType.ts
  18. 0 1
      packages/common/src/Domain/index.ts
  19. 39 0
      packages/domain-core/src/Domain/Common/ContentType.spec.ts
  20. 79 0
      packages/domain-core/src/Domain/Common/ContentType.ts
  21. 0 0
      packages/domain-core/src/Domain/Common/ContentTypeProps.ts
  22. 2 0
      packages/domain-core/src/Domain/index.ts
  23. 1 1
      packages/revisions/package.json
  24. 0 16
      packages/revisions/src/Domain/Revision/ContentType.spec.ts
  25. 0 22
      packages/revisions/src/Domain/Revision/ContentType.ts
  26. 2 2
      packages/revisions/src/Domain/Revision/Revision.spec.ts
  27. 1 3
      packages/revisions/src/Domain/Revision/RevisionMetadataProps.ts
  28. 1 3
      packages/revisions/src/Domain/Revision/RevisionProps.ts
  29. 1 2
      packages/revisions/src/Mapping/RevisionItemStringMapper.ts
  30. 1 2
      packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts
  31. 1 2
      packages/revisions/src/Mapping/RevisionPersistenceMapper.ts
  32. 1 1
      packages/syncing-server/package.json
  33. 105 89
      packages/syncing-server/src/Bootstrap/Container.ts
  34. 3 3
      packages/syncing-server/src/Bootstrap/DataSource.ts
  35. 7 6
      packages/syncing-server/src/Bootstrap/Types.ts
  36. 17 3
      packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.spec.ts
  37. 2 2
      packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.ts
  38. 17 4
      packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts
  39. 35 9
      packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts
  40. 4 4
      packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts
  41. 17 4
      packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts
  42. 1 2
      packages/syncing-server/src/Domain/Item/ExtendedIntegrityPayload.ts
  43. 16 113
      packages/syncing-server/src/Domain/Item/Item.ts
  44. 0 198
      packages/syncing-server/src/Domain/Item/ItemFactory.spec.ts
  45. 0 79
      packages/syncing-server/src/Domain/Item/ItemFactory.ts
  46. 0 7
      packages/syncing-server/src/Domain/Item/ItemFactoryInterface.ts
  47. 1 3
      packages/syncing-server/src/Domain/Item/ItemHash.ts
  48. 16 0
      packages/syncing-server/src/Domain/Item/ItemProps.ts
  49. 2 2
      packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts
  50. 203 388
      packages/syncing-server/src/Domain/Item/ItemService.spec.ts
  51. 45 117
      packages/syncing-server/src/Domain/Item/ItemService.ts
  52. 4 5
      packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.spec.ts
  53. 3 3
      packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.spec.ts
  54. 4 5
      packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts
  55. 47 13
      packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.spec.ts
  56. 14 1
      packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts
  57. 28 19
      packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts
  58. 1 1
      packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts
  59. 0 72
      packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.spec.ts
  60. 0 25
      packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts
  61. 4 4
      packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts
  62. 6 6
      packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts
  63. 54 21
      packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts
  64. 12 14
      packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts
  65. 23 22
      packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts
  66. 11 10
      packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts
  67. 7 6
      packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.spec.ts
  68. 3 4
      packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.ts
  69. 264 0
      packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.spec.ts
  70. 121 0
      packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts
  71. 7 0
      packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItemDTO.ts
  72. 50 12
      packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts
  73. 3 2
      packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts
  74. 251 0
      packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts
  75. 135 0
      packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts
  76. 8 0
      packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItemDTO.ts
  77. 5 5
      packages/syncing-server/src/Infra/FS/FSItemBackupService.ts
  78. 4 5
      packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController.ts
  79. 7 9
      packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts
  80. 4 4
      packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.ts
  81. 13 8
      packages/syncing-server/src/Infra/S3/S3ItemBackupService.ts
  82. 118 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMItem.ts
  83. 29 10
      packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts
  84. 32 0
      packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts
  85. 16 0
      packages/syncing-server/src/Mapping/Backup/ItemBackupRepresentation.ts
  86. 27 0
      packages/syncing-server/src/Mapping/Http/ItemConflictHttpMapper.ts
  87. 10 0
      packages/syncing-server/src/Mapping/Http/ItemConflictHttpRepresentation.ts
  88. 31 0
      packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts
  89. 1 1
      packages/syncing-server/src/Mapping/Http/ItemHttpRepresentation.ts
  90. 27 0
      packages/syncing-server/src/Mapping/Http/SavedItemHttpMapper.ts
  91. 1 1
      packages/syncing-server/src/Mapping/Http/SavedItemHttpRepresentation.ts
  92. 96 0
      packages/syncing-server/src/Mapping/Persistence/ItemPersistenceMapper.ts
  93. 0 10
      packages/syncing-server/src/Projection/ItemConflictProjection.ts
  94. 0 75
      packages/syncing-server/src/Projection/ItemConflictProjector.spec.ts
  95. 0 31
      packages/syncing-server/src/Projection/ItemConflictProjector.ts
  96. 0 5
      packages/syncing-server/src/Projection/ItemProjectionWithUser.ts
  97. 0 75
      packages/syncing-server/src/Projection/ItemProjector.spec.ts
  98. 0 41
      packages/syncing-server/src/Projection/ItemProjector.ts
  99. 0 5
      packages/syncing-server/src/Projection/ProjectorInterface.ts
  100. 0 64
      packages/syncing-server/src/Projection/SavedItemProjector.spec.ts

+ 58 - 32
.pnp.cjs

@@ -4560,17 +4560,16 @@ const RAW_RUNTIME_STATE =
       }]\
       }]\
     ]],\
     ]],\
     ["@standardnotes/api", [\
     ["@standardnotes/api", [\
-      ["npm:1.26.10", {\
-        "packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.26.10-f6165cafd3-3c3561aec8.zip/node_modules/@standardnotes/api/",\
+      ["npm:1.26.25", {\
+        "packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip/node_modules/@standardnotes/api/",\
         "packageDependencies": [\
         "packageDependencies": [\
-          ["@standardnotes/api", "npm:1.26.10"],\
+          ["@standardnotes/api", "npm:1.26.25"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
-          ["@standardnotes/encryption", "npm:1.21.38"],\
-          ["@standardnotes/models", "npm:1.45.5"],\
-          ["@standardnotes/responses", "npm:1.13.24"],\
+          ["@standardnotes/models", "npm:1.46.7"],\
+          ["@standardnotes/responses", "npm:1.13.26"],\
           ["@standardnotes/security", "workspace:packages/security"],\
           ["@standardnotes/security", "workspace:packages/security"],\
-          ["@standardnotes/utils", "npm:1.16.5"],\
+          ["@standardnotes/utils", "npm:1.17.4"],\
           ["reflect-metadata", "npm:0.1.13"]\
           ["reflect-metadata", "npm:0.1.13"]\
         ],\
         ],\
         "linkType": "HARD"\
         "linkType": "HARD"\
@@ -4635,7 +4634,7 @@ const RAW_RUNTIME_STATE =
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
           ["@simplewebauthn/server", "npm:7.2.0"],\
           ["@simplewebauthn/server", "npm:7.2.0"],\
           ["@simplewebauthn/typescript-types", "npm:7.0.0"],\
           ["@simplewebauthn/typescript-types", "npm:7.0.0"],\
-          ["@standardnotes/api", "npm:1.26.10"],\
+          ["@standardnotes/api", "npm:1.26.25"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -4781,21 +4780,6 @@ const RAW_RUNTIME_STATE =
         "linkType": "SOFT"\
         "linkType": "SOFT"\
       }]\
       }]\
     ]],\
     ]],\
-    ["@standardnotes/encryption", [\
-      ["npm:1.21.38", {\
-        "packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.21.38-d08c3d4766-1393840523.zip/node_modules/@standardnotes/encryption/",\
-        "packageDependencies": [\
-          ["@standardnotes/encryption", "npm:1.21.38"],\
-          ["@standardnotes/common", "workspace:packages/common"],\
-          ["@standardnotes/models", "npm:1.45.5"],\
-          ["@standardnotes/responses", "npm:1.13.24"],\
-          ["@standardnotes/sncrypto-common", "npm:1.13.3"],\
-          ["@standardnotes/utils", "npm:1.16.5"],\
-          ["reflect-metadata", "npm:0.1.13"]\
-        ],\
-        "linkType": "HARD"\
-      }]\
-    ]],\
     ["@standardnotes/event-store", [\
     ["@standardnotes/event-store", [\
       ["workspace:packages/event-store", {\
       ["workspace:packages/event-store", {\
         "packageLocation": "./packages/event-store/",\
         "packageLocation": "./packages/event-store/",\
@@ -4841,6 +4825,17 @@ const RAW_RUNTIME_STATE =
           ["reflect-metadata", "npm:0.1.13"]\
           ["reflect-metadata", "npm:0.1.13"]\
         ],\
         ],\
         "linkType": "HARD"\
         "linkType": "HARD"\
+      }],\
+      ["npm:1.59.6", {\
+        "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip/node_modules/@standardnotes/features/",\
+        "packageDependencies": [\
+          ["@standardnotes/features", "npm:1.59.6"],\
+          ["@standardnotes/common", "workspace:packages/common"],\
+          ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
+          ["@standardnotes/security", "workspace:packages/security"],\
+          ["reflect-metadata", "npm:0.1.13"]\
+        ],\
+        "linkType": "HARD"\
       }]\
       }]\
     ]],\
     ]],\
     ["@standardnotes/files-server", [\
     ["@standardnotes/files-server", [\
@@ -4935,14 +4930,15 @@ const RAW_RUNTIME_STATE =
       }]\
       }]\
     ]],\
     ]],\
     ["@standardnotes/models", [\
     ["@standardnotes/models", [\
-      ["npm:1.45.5", {\
-        "packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.45.5-29326e959c-15f26c11b2.zip/node_modules/@standardnotes/models/",\
+      ["npm:1.46.7", {\
+        "packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip/node_modules/@standardnotes/models/",\
         "packageDependencies": [\
         "packageDependencies": [\
-          ["@standardnotes/models", "npm:1.45.5"],\
+          ["@standardnotes/models", "npm:1.46.7"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/common", "workspace:packages/common"],\
-          ["@standardnotes/features", "npm:1.59.5"],\
-          ["@standardnotes/responses", "npm:1.13.24"],\
-          ["@standardnotes/utils", "npm:1.16.5"],\
+          ["@standardnotes/features", "npm:1.59.6"],\
+          ["@standardnotes/responses", "npm:1.13.26"],\
+          ["@standardnotes/sncrypto-common", "npm:1.13.4"],\
+          ["@standardnotes/utils", "npm:1.17.4"],\
           ["lodash", "npm:4.17.21"]\
           ["lodash", "npm:4.17.21"]\
         ],\
         ],\
         "linkType": "HARD"\
         "linkType": "HARD"\
@@ -4977,6 +4973,17 @@ const RAW_RUNTIME_STATE =
           ["reflect-metadata", "npm:0.1.13"]\
           ["reflect-metadata", "npm:0.1.13"]\
         ],\
         ],\
         "linkType": "HARD"\
         "linkType": "HARD"\
+      }],\
+      ["npm:1.13.26", {\
+        "packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip/node_modules/@standardnotes/responses/",\
+        "packageDependencies": [\
+          ["@standardnotes/responses", "npm:1.13.26"],\
+          ["@standardnotes/common", "workspace:packages/common"],\
+          ["@standardnotes/features", "npm:1.59.6"],\
+          ["@standardnotes/security", "workspace:packages/security"],\
+          ["reflect-metadata", "npm:0.1.13"]\
+        ],\
+        "linkType": "HARD"\
       }]\
       }]\
     ]],\
     ]],\
     ["@standardnotes/revisions-server", [\
     ["@standardnotes/revisions-server", [\
@@ -4987,7 +4994,7 @@ const RAW_RUNTIME_STATE =
           ["@aws-sdk/client-s3", "npm:3.342.0"],\
           ["@aws-sdk/client-s3", "npm:3.342.0"],\
           ["@aws-sdk/client-sqs", "npm:3.342.0"],\
           ["@aws-sdk/client-sqs", "npm:3.342.0"],\
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
-          ["@standardnotes/api", "npm:1.26.10"],\
+          ["@standardnotes/api", "npm:1.26.25"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -5136,6 +5143,14 @@ const RAW_RUNTIME_STATE =
           ["reflect-metadata", "npm:0.1.13"]\
           ["reflect-metadata", "npm:0.1.13"]\
         ],\
         ],\
         "linkType": "HARD"\
         "linkType": "HARD"\
+      }],\
+      ["npm:1.13.4", {\
+        "packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip/node_modules/@standardnotes/sncrypto-common/",\
+        "packageDependencies": [\
+          ["@standardnotes/sncrypto-common", "npm:1.13.4"],\
+          ["reflect-metadata", "npm:0.1.13"]\
+        ],\
+        "linkType": "HARD"\
       }]\
       }]\
     ]],\
     ]],\
     ["@standardnotes/sncrypto-node", [\
     ["@standardnotes/sncrypto-node", [\
@@ -5171,7 +5186,7 @@ const RAW_RUNTIME_STATE =
           ["@aws-sdk/client-sns", "npm:3.342.0"],\
           ["@aws-sdk/client-sns", "npm:3.342.0"],\
           ["@aws-sdk/client-sqs", "npm:3.342.0"],\
           ["@aws-sdk/client-sqs", "npm:3.342.0"],\
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
-          ["@standardnotes/api", "npm:1.26.10"],\
+          ["@standardnotes/api", "npm:1.26.25"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -5257,6 +5272,17 @@ const RAW_RUNTIME_STATE =
           ["reflect-metadata", "npm:0.1.13"]\
           ["reflect-metadata", "npm:0.1.13"]\
         ],\
         ],\
         "linkType": "HARD"\
         "linkType": "HARD"\
+      }],\
+      ["npm:1.17.4", {\
+        "packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip/node_modules/@standardnotes/utils/",\
+        "packageDependencies": [\
+          ["@standardnotes/utils", "npm:1.17.4"],\
+          ["@standardnotes/common", "workspace:packages/common"],\
+          ["dompurify", "npm:2.4.5"],\
+          ["lodash", "npm:4.17.21"],\
+          ["reflect-metadata", "npm:0.1.13"]\
+        ],\
+        "linkType": "HARD"\
       }]\
       }]\
     ]],\
     ]],\
     ["@standardnotes/websockets-server", [\
     ["@standardnotes/websockets-server", [\
@@ -5266,7 +5292,7 @@ const RAW_RUNTIME_STATE =
           ["@standardnotes/websockets-server", "workspace:packages/websockets"],\
           ["@standardnotes/websockets-server", "workspace:packages/websockets"],\
           ["@aws-sdk/client-sqs", "npm:3.342.0"],\
           ["@aws-sdk/client-sqs", "npm:3.342.0"],\
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\
-          ["@standardnotes/api", "npm:1.26.10"],\
+          ["@standardnotes/api", "npm:1.26.25"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/common", "workspace:packages/common"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\

二进制
.yarn/cache/@standardnotes-api-npm-1.26.10-f6165cafd3-3c3561aec8.zip


二进制
.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip


二进制
.yarn/cache/@standardnotes-encryption-npm-1.21.38-d08c3d4766-1393840523.zip


二进制
.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip


二进制
.yarn/cache/@standardnotes-models-npm-1.45.5-29326e959c-15f26c11b2.zip


二进制
.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip


二进制
.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip


二进制
.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip


二进制
.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip


+ 1 - 1
packages/auth/package.json

@@ -42,7 +42,7 @@
     "@cbor-extract/cbor-extract-linux-x64": "^2.1.1",
     "@cbor-extract/cbor-extract-linux-x64": "^2.1.1",
     "@simplewebauthn/server": "^7.2.0",
     "@simplewebauthn/server": "^7.2.0",
     "@simplewebauthn/typescript-types": "^7.0.0",
     "@simplewebauthn/typescript-types": "^7.0.0",
-    "@standardnotes/api": "^1.25.3",
+    "@standardnotes/api": "^1.26.25",
     "@standardnotes/common": "workspace:*",
     "@standardnotes/common": "workspace:*",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events": "workspace:*",
     "@standardnotes/domain-events": "workspace:*",

+ 4 - 4
packages/auth/src/Controller/AuthController.spec.ts

@@ -8,12 +8,12 @@ import { User } from '../Domain/User/User'
 import { Register } from '../Domain/UseCase/Register'
 import { Register } from '../Domain/UseCase/Register'
 import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
 import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
 import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
 import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
-import { ApiVersion } from '@standardnotes/api'
 import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
 import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
 import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
 import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
 import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
 import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
 import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
 import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
+import { ApiVersion } from '../Domain/Api/ApiVersion'
 
 
 describe('AuthController', () => {
 describe('AuthController', () => {
   let clearLoginAttempts: ClearLoginAttempts
   let clearLoginAttempts: ClearLoginAttempts
@@ -73,7 +73,7 @@ describe('AuthController', () => {
       email: 'test@test.te',
       email: 'test@test.te',
       password: 'asdzxc',
       password: 'asdzxc',
       version: ProtocolVersion.V004,
       version: ProtocolVersion.V004,
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       origination: KeyParamsOrigination.Registration,
       origination: KeyParamsOrigination.Registration,
       userAgent: 'Google Chrome',
       userAgent: 'Google Chrome',
       identifier: 'test@test.te',
       identifier: 'test@test.te',
@@ -103,7 +103,7 @@ describe('AuthController', () => {
       email: 'test@test.te',
       email: 'test@test.te',
       password: '',
       password: '',
       version: ProtocolVersion.V004,
       version: ProtocolVersion.V004,
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       origination: KeyParamsOrigination.Registration,
       origination: KeyParamsOrigination.Registration,
       userAgent: 'Google Chrome',
       userAgent: 'Google Chrome',
       identifier: 'test@test.te',
       identifier: 'test@test.te',
@@ -123,7 +123,7 @@ describe('AuthController', () => {
       email: 'test@test.te',
       email: 'test@test.te',
       password: 'test',
       password: 'test',
       version: ProtocolVersion.V004,
       version: ProtocolVersion.V004,
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       origination: KeyParamsOrigination.Registration,
       origination: KeyParamsOrigination.Registration,
       userAgent: 'Google Chrome',
       userAgent: 'Google Chrome',
       identifier: 'test@test.te',
       identifier: 'test@test.te',

+ 9 - 3
packages/auth/src/Controller/AuthController.ts

@@ -1,10 +1,10 @@
 import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import {
 import {
-  ApiVersion,
   UserRegistrationRequestParams,
   UserRegistrationRequestParams,
   UserServerInterface,
   UserServerInterface,
   UserDeletionResponseBody,
   UserDeletionResponseBody,
   UserRegistrationResponseBody,
   UserRegistrationResponseBody,
+  UserUpdateRequestParams,
 } from '@standardnotes/api'
 } from '@standardnotes/api'
 import { ErrorTag, HttpResponse, HttpStatusCode } from '@standardnotes/responses'
 import { ErrorTag, HttpResponse, HttpStatusCode } from '@standardnotes/responses'
 import { ProtocolVersion } from '@standardnotes/common'
 import { ProtocolVersion } from '@standardnotes/common'
@@ -23,6 +23,8 @@ import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/G
 import { GenerateRecoveryCodesRequestParams } from '../Infra/Http/Request/GenerateRecoveryCodesRequestParams'
 import { GenerateRecoveryCodesRequestParams } from '../Infra/Http/Request/GenerateRecoveryCodesRequestParams'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
 import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
 import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
+import { ApiVersion } from '../Domain/Api/ApiVersion'
+import { UserUpdateResponse } from '@standardnotes/api/dist/Domain/Response/User/UserUpdateResponse'
 
 
 export class AuthController implements UserServerInterface {
 export class AuthController implements UserServerInterface {
   constructor(
   constructor(
@@ -37,6 +39,10 @@ export class AuthController implements UserServerInterface {
     private sessionService: SessionServiceInterface,
     private sessionService: SessionServiceInterface,
   ) {}
   ) {}
 
 
+  async update(_params: UserUpdateRequestParams): Promise<HttpResponse<UserUpdateResponse>> {
+    throw new Error('Method not implemented.')
+  }
+
   async deleteAccount(_params: never): Promise<HttpResponse<UserDeletionResponseBody>> {
   async deleteAccount(_params: never): Promise<HttpResponse<UserDeletionResponseBody>> {
     throw new Error('This method is implemented on the payments server.')
     throw new Error('This method is implemented on the payments server.')
   }
   }
@@ -121,7 +127,7 @@ export class AuthController implements UserServerInterface {
   async signInWithRecoveryCodes(
   async signInWithRecoveryCodes(
     params: SignInWithRecoveryCodesRequestParams,
     params: SignInWithRecoveryCodesRequestParams,
   ): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
   ): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
-    if (params.apiVersion !== ApiVersion.v0) {
+    if (params.apiVersion !== ApiVersion.v20200115) {
       return {
       return {
         status: HttpStatusCode.BadRequest,
         status: HttpStatusCode.BadRequest,
         data: {
         data: {
@@ -162,7 +168,7 @@ export class AuthController implements UserServerInterface {
   async recoveryKeyParams(
   async recoveryKeyParams(
     params: RecoveryKeyParamsRequestParams,
     params: RecoveryKeyParamsRequestParams,
   ): Promise<HttpResponse<RecoveryKeyParamsResponseBody>> {
   ): Promise<HttpResponse<RecoveryKeyParamsResponseBody>> {
-    if (params.apiVersion !== ApiVersion.v0) {
+    if (params.apiVersion !== ApiVersion.v20200115) {
       return {
       return {
         status: HttpStatusCode.BadRequest,
         status: HttpStatusCode.BadRequest,
         data: {
         data: {

+ 11 - 11
packages/auth/src/Controller/SubscriptionInvitesController.spec.ts

@@ -7,7 +7,7 @@ import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptShar
 import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSharedSubscriptionInvitation/DeclineSharedSubscriptionInvitation'
 import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSharedSubscriptionInvitation/DeclineSharedSubscriptionInvitation'
 import { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation'
 import { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation'
 import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
 import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
-import { ApiVersion } from '@standardnotes/api'
+import { ApiVersion } from '../Domain/Api/ApiVersion'
 
 
 describe('SubscriptionInvitesController', () => {
 describe('SubscriptionInvitesController', () => {
   let inviteToSharedSubscription: InviteToSharedSubscription
   let inviteToSharedSubscription: InviteToSharedSubscription
@@ -53,7 +53,7 @@ describe('SubscriptionInvitesController', () => {
       invitations: [],
       invitations: [],
     })
     })
 
 
-    const result = await createController().listInvites({ api: ApiVersion.v0, inviterEmail: 'test@test.te' })
+    const result = await createController().listInvites({ api: ApiVersion.v20200115, inviterEmail: 'test@test.te' })
 
 
     expect(listSharedSubscriptionInvitations.execute).toHaveBeenCalledWith({
     expect(listSharedSubscriptionInvitations.execute).toHaveBeenCalledWith({
       inviterEmail: 'test@test.te',
       inviterEmail: 'test@test.te',
@@ -68,7 +68,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().cancelInvite({
     const result = await createController().cancelInvite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       inviteUuid: '1-2-3',
       inviteUuid: '1-2-3',
       inviterEmail: 'test@test.te',
       inviterEmail: 'test@test.te',
     })
     })
@@ -87,7 +87,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().cancelInvite({
     const result = await createController().cancelInvite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       inviteUuid: '1-2-3',
       inviteUuid: '1-2-3',
     })
     })
 
 
@@ -100,7 +100,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().declineInvite({
     const result = await createController().declineInvite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       inviteUuid: '1-2-3',
       inviteUuid: '1-2-3',
     })
     })
 
 
@@ -117,7 +117,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().declineInvite({
     const result = await createController().declineInvite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       inviteUuid: '1-2-3',
       inviteUuid: '1-2-3',
     })
     })
 
 
@@ -134,7 +134,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().acceptInvite({
     const result = await createController().acceptInvite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       inviteUuid: '1-2-3',
       inviteUuid: '1-2-3',
     })
     })
 
 
@@ -151,7 +151,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().acceptInvite({
     const result = await createController().acceptInvite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       inviteUuid: '1-2-3',
       inviteUuid: '1-2-3',
     })
     })
 
 
@@ -168,7 +168,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().invite({
     const result = await createController().invite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       identifier: 'invitee@test.te',
       identifier: 'invitee@test.te',
       inviterUuid: '1-2-3',
       inviterUuid: '1-2-3',
       inviterEmail: 'test@test.te',
       inviterEmail: 'test@test.te',
@@ -187,7 +187,7 @@ describe('SubscriptionInvitesController', () => {
 
 
   it('should not invite to user subscription if the identifier is missing in request', async () => {
   it('should not invite to user subscription if the identifier is missing in request', async () => {
     const result = await createController().invite({
     const result = await createController().invite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       identifier: '',
       identifier: '',
       inviterUuid: '1-2-3',
       inviterUuid: '1-2-3',
       inviterEmail: 'test@test.te',
       inviterEmail: 'test@test.te',
@@ -205,7 +205,7 @@ describe('SubscriptionInvitesController', () => {
     })
     })
 
 
     const result = await createController().invite({
     const result = await createController().invite({
-      api: ApiVersion.v0,
+      api: ApiVersion.v20200115,
       identifier: 'invitee@test.te',
       identifier: 'invitee@test.te',
       inviterUuid: '1-2-3',
       inviterUuid: '1-2-3',
       inviterEmail: 'test@test.te',
       inviterEmail: 'test@test.te',

+ 2 - 2
packages/auth/src/Domain/UseCase/Register.ts

@@ -1,6 +1,5 @@
 import * as bcrypt from 'bcryptjs'
 import * as bcrypt from 'bcryptjs'
 import { RoleName, Username } from '@standardnotes/domain-core'
 import { RoleName, Username } from '@standardnotes/domain-core'
-import { ApiVersion } from '@standardnotes/api'
 
 
 import { v4 as uuidv4 } from 'uuid'
 import { v4 as uuidv4 } from 'uuid'
 import { inject, injectable } from 'inversify'
 import { inject, injectable } from 'inversify'
@@ -16,6 +15,7 @@ import { TimerInterface } from '@standardnotes/time'
 import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
 import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
 import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
 import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
 import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
 import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
+import { ApiVersion } from '../Api/ApiVersion'
 
 
 @injectable()
 @injectable()
 export class Register implements UseCaseInterface {
 export class Register implements UseCaseInterface {
@@ -39,7 +39,7 @@ export class Register implements UseCaseInterface {
 
 
     const { email, password, apiVersion, ephemeralSession, ...registrationFields } = dto
     const { email, password, apiVersion, ephemeralSession, ...registrationFields } = dto
 
 
-    if (apiVersion !== ApiVersion.v0) {
+    if (apiVersion !== ApiVersion.v20200115) {
       return {
       return {
         success: false,
         success: false,
         errorMessage: `Unsupported api version: ${apiVersion}`,
         errorMessage: `Unsupported api version: ${apiVersion}`,

+ 2 - 2
packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts

@@ -1,7 +1,6 @@
 import * as bcrypt from 'bcryptjs'
 import * as bcrypt from 'bcryptjs'
 import { Result, UseCaseInterface, Username, Uuid, Validator } from '@standardnotes/domain-core'
 import { Result, UseCaseInterface, Username, Uuid, Validator } from '@standardnotes/domain-core'
 import { SettingName } from '@standardnotes/settings'
 import { SettingName } from '@standardnotes/settings'
-import { ApiVersion } from '@standardnotes/api'
 
 
 import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
 import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
 import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
 import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
@@ -16,6 +15,7 @@ import { IncreaseLoginAttempts } from '../IncreaseLoginAttempts'
 import { ClearLoginAttempts } from '../ClearLoginAttempts'
 import { ClearLoginAttempts } from '../ClearLoginAttempts'
 import { DeleteSetting } from '../DeleteSetting/DeleteSetting'
 import { DeleteSetting } from '../DeleteSetting/DeleteSetting'
 import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
 import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
+import { ApiVersion } from '../../Api/ApiVersion'
 
 
 export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse20200115> {
 export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse20200115> {
   constructor(
   constructor(
@@ -100,7 +100,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
 
 
     const authResponse = await this.authResponseFactory.createResponse({
     const authResponse = await this.authResponseFactory.createResponse({
       user,
       user,
-      apiVersion: ApiVersion.v0,
+      apiVersion: ApiVersion.v20200115,
       userAgent: dto.userAgent,
       userAgent: dto.userAgent,
       ephemeralSession: false,
       ephemeralSession: false,
       readonlyAccess: false,
       readonlyAccess: false,

+ 0 - 48
packages/common/src/Domain/Content/ContentType.ts

@@ -1,48 +0,0 @@
-/* istanbul ignore file */
-export enum ContentType {
-  Any = '*',
-  Item = 'SF|Item',
-  KeySystemItemsKey = 'SN|KeySystemItemsKey',
-  KeySystemRootKey = 'SN|KeySystemRootKey',
-  TrustedContact = 'SN|TrustedContact',
-  VaultListing = 'SN|VaultListing',
-  RootKey = 'SN|RootKey|NoSync',
-  ItemsKey = 'SN|ItemsKey',
-  EncryptedStorage = 'SN|EncryptedStorage',
-  Privileges = 'SN|Privileges',
-  Note = 'Note',
-  Tag = 'Tag',
-  SmartView = 'SN|SmartTag',
-  Component = 'SN|Component',
-  Editor = 'SN|Editor',
-  ActionsExtension = 'Extension',
-  UserPrefs = 'SN|UserPreferences',
-  HistorySession = 'SN|HistorySession',
-  Theme = 'SN|Theme',
-  File = 'SN|File',
-  FilesafeCredentials = 'SN|FileSafe|Credentials',
-  FilesafeFileMetadata = 'SN|FileSafe|FileMetadata',
-  FilesafeIntegration = 'SN|FileSafe|Integration',
-  ExtensionRepo = 'SN|ExtensionRepo',
-  Unknown = 'Unknown',
-}
-
-export function DisplayStringForContentType(contentType: ContentType): string | undefined {
-  const map: Partial<Record<ContentType, string>> = {
-    [ContentType.ActionsExtension]: 'action-based extension',
-    [ContentType.Component]: 'component',
-    [ContentType.Editor]: 'editor',
-    [ContentType.File]: 'file',
-    [ContentType.FilesafeCredentials]: 'FileSafe credential',
-    [ContentType.FilesafeFileMetadata]: 'FileSafe file',
-    [ContentType.FilesafeIntegration]: 'FileSafe integration',
-    [ContentType.ItemsKey]: 'encryption key',
-    [ContentType.Note]: 'note',
-    [ContentType.SmartView]: 'smart view',
-    [ContentType.Tag]: 'tag',
-    [ContentType.Theme]: 'theme',
-    [ContentType.UserPrefs]: 'user preferences',
-  }
-
-  return map[contentType]
-}

+ 0 - 1
packages/common/src/Domain/index.ts

@@ -1,4 +1,3 @@
-export * from './Content/ContentType'
 export * from './Content/ContentDecoder'
 export * from './Content/ContentDecoder'
 export * from './Content/ContentDecoderInterface'
 export * from './Content/ContentDecoderInterface'
 export * from './DataType/AnyRecord'
 export * from './DataType/AnyRecord'

+ 39 - 0
packages/domain-core/src/Domain/Common/ContentType.spec.ts

@@ -0,0 +1,39 @@
+import { ContentType } from './ContentType'
+
+describe('ContentType', () => {
+  it('should create a value object', () => {
+    const valueOrError = ContentType.create(ContentType.TYPES.Component)
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+    expect(valueOrError.getValue().value).toEqual('SN|Component')
+  })
+
+  it('should not create an invalid value object', () => {
+    for (const value of ['', undefined, 0, 'FOOBAR']) {
+      const valueOrError = ContentType.create(value as string)
+
+      expect(valueOrError.isFailed()).toBeTruthy()
+    }
+  })
+
+  it('should return a display name', () => {
+    const valueOrError = ContentType.create(ContentType.TYPES.FilesafeFileMetadata)
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+    expect(valueOrError.getValue().getDisplayName()).toEqual('FileSafe file')
+  })
+
+  it('should return null for a display name if the value is null', () => {
+    const valueOrError = ContentType.create(null)
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+    expect(valueOrError.getValue().getDisplayName()).toBeNull()
+  })
+
+  it('should fallback to the value if the display name is not found', () => {
+    const valueOrError = ContentType.create(ContentType.TYPES.Unknown)
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+    expect(valueOrError.getValue().getDisplayName()).toEqual('Unknown')
+  })
+})

+ 79 - 0
packages/domain-core/src/Domain/Common/ContentType.ts

@@ -0,0 +1,79 @@
+import { Result } from '../Core/Result'
+import { ValueObject } from '../Core/ValueObject'
+
+import { ContentTypeProps } from './ContentTypeProps'
+
+export class ContentType extends ValueObject<ContentTypeProps> {
+  static readonly TYPES = {
+    Any: '*',
+    Item: 'SF|Item',
+    KeySystemItemsKey: 'SN|KeySystemItemsKey',
+    KeySystemRootKey: 'SN|KeySystemRootKey',
+    TrustedContact: 'SN|TrustedContact',
+    VaultListing: 'SN|VaultListing',
+    RootKey: 'SN|RootKey|NoSync',
+    ItemsKey: 'SN|ItemsKey',
+    EncryptedStorage: 'SN|EncryptedStorage',
+    Privileges: 'SN|Privileges',
+    Note: 'Note',
+    Tag: 'Tag',
+    SmartView: 'SN|SmartTag',
+    Component: 'SN|Component',
+    Editor: 'SN|Editor',
+    ActionsExtension: 'Extension',
+    UserPrefs: 'SN|UserPreferences',
+    HistorySession: 'SN|HistorySession',
+    Theme: 'SN|Theme',
+    File: 'SN|File',
+    FilesafeCredentials: 'SN|FileSafe|Credentials',
+    FilesafeFileMetadata: 'SN|FileSafe|FileMetadata',
+    FilesafeIntegration: 'SN|FileSafe|Integration',
+    ExtensionRepo: 'SN|ExtensionRepo',
+    Unknown: 'Unknown',
+  }
+
+  private readonly displayNamesMap: Partial<Record<string, string>> = {
+    [ContentType.TYPES.ActionsExtension]: 'action-based extension',
+    [ContentType.TYPES.Component]: 'component',
+    [ContentType.TYPES.Editor]: 'editor',
+    [ContentType.TYPES.File]: 'file',
+    [ContentType.TYPES.FilesafeCredentials]: 'FileSafe credential',
+    [ContentType.TYPES.FilesafeFileMetadata]: 'FileSafe file',
+    [ContentType.TYPES.FilesafeIntegration]: 'FileSafe integration',
+    [ContentType.TYPES.ItemsKey]: 'encryption key',
+    [ContentType.TYPES.Note]: 'note',
+    [ContentType.TYPES.SmartView]: 'smart view',
+    [ContentType.TYPES.Tag]: 'tag',
+    [ContentType.TYPES.Theme]: 'theme',
+    [ContentType.TYPES.UserPrefs]: 'user preferences',
+  }
+
+  get value(): string | null {
+    return this.props.value
+  }
+
+  private constructor(props: ContentTypeProps) {
+    super(props)
+  }
+
+  static create(type: string | null): Result<ContentType> {
+    if (type === null) {
+      return Result.ok<ContentType>(new ContentType({ value: null }))
+    }
+
+    const isValidType = Object.values(this.TYPES).includes(type)
+    if (!isValidType) {
+      return Result.fail<ContentType>(`Invalid content type: ${type}`)
+    } else {
+      return Result.ok<ContentType>(new ContentType({ value: type }))
+    }
+  }
+
+  getDisplayName(): string | null {
+    if (!this.value) {
+      return null
+    }
+
+    return this.displayNamesMap[this.value] || this.value
+  }
+}

+ 0 - 0
packages/revisions/src/Domain/Revision/ContentTypeProps.ts → packages/domain-core/src/Domain/Common/ContentTypeProps.ts


+ 2 - 0
packages/domain-core/src/Domain/index.ts

@@ -9,6 +9,8 @@ export * from './Cache/CacheEntry'
 export * from './Cache/CacheEntryProps'
 export * from './Cache/CacheEntryProps'
 export * from './Cache/CacheEntryRepositoryInterface'
 export * from './Cache/CacheEntryRepositoryInterface'
 
 
+export * from './Common/ContentType'
+export * from './Common/ContentTypeProps'
 export * from './Common/Dates'
 export * from './Common/Dates'
 export * from './Common/DatesProps'
 export * from './Common/DatesProps'
 export * from './Common/Email'
 export * from './Common/Email'

+ 1 - 1
packages/revisions/package.json

@@ -27,7 +27,7 @@
   "dependencies": {
   "dependencies": {
     "@aws-sdk/client-s3": "^3.332.0",
     "@aws-sdk/client-s3": "^3.332.0",
     "@aws-sdk/client-sqs": "^3.332.0",
     "@aws-sdk/client-sqs": "^3.332.0",
-    "@standardnotes/api": "^1.25.3",
+    "@standardnotes/api": "^1.26.25",
     "@standardnotes/common": "workspace:^",
     "@standardnotes/common": "workspace:^",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events": "workspace:*",
     "@standardnotes/domain-events": "workspace:*",

+ 0 - 16
packages/revisions/src/Domain/Revision/ContentType.spec.ts

@@ -1,16 +0,0 @@
-import { ContentType } from './ContentType'
-
-describe('ContentType', () => {
-  it('should create a value obejct', () => {
-    const valueOrError = ContentType.create('Note')
-
-    expect(valueOrError.isFailed()).toBeFalsy()
-    expect(valueOrError.getValue().value).not.toBeNull()
-  })
-
-  it('should fail to create a value obejct', () => {
-    const valueOrError = ContentType.create('test')
-
-    expect(valueOrError.isFailed()).toBeTruthy()
-  })
-})

+ 0 - 22
packages/revisions/src/Domain/Revision/ContentType.ts

@@ -1,22 +0,0 @@
-import { ContentType as ContentTypeValues } from '@standardnotes/common'
-import { Result, ValueObject } from '@standardnotes/domain-core'
-
-import { ContentTypeProps } from './ContentTypeProps'
-
-export class ContentType extends ValueObject<ContentTypeProps> {
-  get value(): string | null {
-    return this.props.value
-  }
-
-  private constructor(props: ContentTypeProps) {
-    super(props)
-  }
-
-  static create(contentType: string | null): Result<ContentType> {
-    if (contentType !== null && !Object.values(ContentTypeValues).includes(contentType as ContentTypeValues)) {
-      return Result.fail<ContentType>(`Value is not a valid content type: ${contentType}`)
-    } else {
-      return Result.ok<ContentType>(new ContentType({ value: contentType }))
-    }
-  }
-}

+ 2 - 2
packages/revisions/src/Domain/Revision/Revision.spec.ts

@@ -1,5 +1,5 @@
-import { Dates, Uuid } from '@standardnotes/domain-core'
-import { ContentType } from './ContentType'
+import { ContentType, Dates, Uuid } from '@standardnotes/domain-core'
+
 import { Revision } from './Revision'
 import { Revision } from './Revision'
 
 
 describe('Revision', () => {
 describe('Revision', () => {

+ 1 - 3
packages/revisions/src/Domain/Revision/RevisionMetadataProps.ts

@@ -1,6 +1,4 @@
-import { Dates } from '@standardnotes/domain-core'
-
-import { ContentType } from './ContentType'
+import { ContentType, Dates } from '@standardnotes/domain-core'
 
 
 export interface RevisionMetadataProps {
 export interface RevisionMetadataProps {
   contentType: ContentType
   contentType: ContentType

+ 1 - 3
packages/revisions/src/Domain/Revision/RevisionProps.ts

@@ -1,6 +1,4 @@
-import { Dates, Uuid } from '@standardnotes/domain-core'
-
-import { ContentType } from './ContentType'
+import { ContentType, Dates, Uuid } from '@standardnotes/domain-core'
 
 
 export interface RevisionProps {
 export interface RevisionProps {
   itemUuid: Uuid
   itemUuid: Uuid

+ 1 - 2
packages/revisions/src/Mapping/RevisionItemStringMapper.ts

@@ -1,6 +1,5 @@
-import { MapperInterface, Dates, Uuid } from '@standardnotes/domain-core'
+import { MapperInterface, Dates, Uuid, ContentType } from '@standardnotes/domain-core'
 
 
-import { ContentType } from '../Domain/Revision/ContentType'
 import { Revision } from '../Domain/Revision/Revision'
 import { Revision } from '../Domain/Revision/Revision'
 
 
 export class RevisionItemStringMapper implements MapperInterface<Revision, string> {
 export class RevisionItemStringMapper implements MapperInterface<Revision, string> {

+ 1 - 2
packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts

@@ -1,6 +1,5 @@
-import { MapperInterface, Dates, UniqueEntityId } from '@standardnotes/domain-core'
+import { MapperInterface, Dates, UniqueEntityId, ContentType } from '@standardnotes/domain-core'
 
 
-import { ContentType } from '../Domain/Revision/ContentType'
 import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
 import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
 import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
 import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
 
 

+ 1 - 2
packages/revisions/src/Mapping/RevisionPersistenceMapper.ts

@@ -1,5 +1,4 @@
-import { MapperInterface, Dates, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
-import { ContentType } from '../Domain/Revision/ContentType'
+import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@standardnotes/domain-core'
 import { Revision } from '../Domain/Revision/Revision'
 import { Revision } from '../Domain/Revision/Revision'
 import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
 import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
 
 

+ 1 - 1
packages/syncing-server/package.json

@@ -31,7 +31,7 @@
     "@aws-sdk/client-s3": "^3.332.0",
     "@aws-sdk/client-s3": "^3.332.0",
     "@aws-sdk/client-sns": "^3.332.0",
     "@aws-sdk/client-sns": "^3.332.0",
     "@aws-sdk/client-sqs": "^3.332.0",
     "@aws-sdk/client-sqs": "^3.332.0",
-    "@standardnotes/api": "^1.25.3",
+    "@standardnotes/api": "^1.26.25",
     "@standardnotes/common": "workspace:*",
     "@standardnotes/common": "workspace:*",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events": "workspace:*",
     "@standardnotes/domain-events": "workspace:*",

+ 105 - 89
packages/syncing-server/src/Bootstrap/Container.ts

@@ -9,9 +9,6 @@ import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
 import { TypeORMItemRepository } from '../Infra/TypeORM/TypeORMItemRepository'
 import { TypeORMItemRepository } from '../Infra/TypeORM/TypeORMItemRepository'
 import { Repository } from 'typeorm'
 import { Repository } from 'typeorm'
 import { Item } from '../Domain/Item/Item'
 import { Item } from '../Domain/Item/Item'
-import { ItemProjection } from '../Projection/ItemProjection'
-import { ProjectorInterface } from '../Projection/ProjectorInterface'
-import { ItemProjector } from '../Projection/ItemProjector'
 import {
 import {
   DirectCallDomainEventPublisher,
   DirectCallDomainEventPublisher,
   DirectCallEventMessageHandler,
   DirectCallEventMessageHandler,
@@ -26,15 +23,12 @@ import { Timer, TimerInterface } from '@standardnotes/time'
 import { ItemTransferCalculatorInterface } from '../Domain/Item/ItemTransferCalculatorInterface'
 import { ItemTransferCalculatorInterface } from '../Domain/Item/ItemTransferCalculatorInterface'
 import { ItemTransferCalculator } from '../Domain/Item/ItemTransferCalculator'
 import { ItemTransferCalculator } from '../Domain/Item/ItemTransferCalculator'
 import { ItemConflict } from '../Domain/Item/ItemConflict'
 import { ItemConflict } from '../Domain/Item/ItemConflict'
-import { ItemFactory } from '../Domain/Item/ItemFactory'
-import { ItemFactoryInterface } from '../Domain/Item/ItemFactoryInterface'
 import { ItemService } from '../Domain/Item/ItemService'
 import { ItemService } from '../Domain/Item/ItemService'
 import { ItemServiceInterface } from '../Domain/Item/ItemServiceInterface'
 import { ItemServiceInterface } from '../Domain/Item/ItemServiceInterface'
 import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter'
 import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter'
 import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter'
 import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter'
 import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter'
 import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter'
 import { TimeDifferenceFilter } from '../Domain/Item/SaveRule/TimeDifferenceFilter'
 import { TimeDifferenceFilter } from '../Domain/Item/SaveRule/TimeDifferenceFilter'
-import { UuidFilter } from '../Domain/Item/SaveRule/UuidFilter'
 import { ItemSaveValidator } from '../Domain/Item/SaveValidator/ItemSaveValidator'
 import { ItemSaveValidator } from '../Domain/Item/SaveValidator/ItemSaveValidator'
 import { ItemSaveValidatorInterface } from '../Domain/Item/SaveValidator/ItemSaveValidatorInterface'
 import { ItemSaveValidatorInterface } from '../Domain/Item/SaveValidator/ItemSaveValidatorInterface'
 import { SyncResponseFactory20161215 } from '../Domain/Item/SyncResponse/SyncResponseFactory20161215'
 import { SyncResponseFactory20161215 } from '../Domain/Item/SyncResponse/SyncResponseFactory20161215'
@@ -45,10 +39,6 @@ import { CheckIntegrity } from '../Domain/UseCase/Syncing/CheckIntegrity/CheckIn
 import { GetItem } from '../Domain/UseCase/Syncing/GetItem/GetItem'
 import { GetItem } from '../Domain/UseCase/Syncing/GetItem/GetItem'
 import { SyncItems } from '../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { SyncItems } from '../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { InversifyExpressAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware'
 import { InversifyExpressAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware'
-import { ItemConflictProjection } from '../Projection/ItemConflictProjection'
-import { ItemConflictProjector } from '../Projection/ItemConflictProjector'
-import { SavedItemProjection } from '../Projection/SavedItemProjection'
-import { SavedItemProjector } from '../Projection/SavedItemProjector'
 import { S3Client } from '@aws-sdk/client-s3'
 import { S3Client } from '@aws-sdk/client-s3'
 import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
 import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
 import { ContentDecoder } from '@standardnotes/common'
 import { ContentDecoder } from '@standardnotes/common'
@@ -70,9 +60,21 @@ import { ItemBackupServiceInterface } from '../Domain/Item/ItemBackupServiceInte
 import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
 import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
 import { AuthHttpService } from '../Infra/HTTP/AuthHttpService'
 import { AuthHttpService } from '../Infra/HTTP/AuthHttpService'
 import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
 import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
-import { ControllerContainer, ControllerContainerInterface } from '@standardnotes/domain-core'
+import { ControllerContainer, ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
 import { HomeServerItemsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController'
 import { HomeServerItemsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController'
 import { Transform } from 'stream'
 import { Transform } from 'stream'
+import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
+import { ItemPersistenceMapper } from '../Mapping/Persistence/ItemPersistenceMapper'
+import { ItemHttpRepresentation } from '../Mapping/Http/ItemHttpRepresentation'
+import { ItemHttpMapper } from '../Mapping/Http/ItemHttpMapper'
+import { SavedItemHttpRepresentation } from '../Mapping/Http/SavedItemHttpRepresentation'
+import { SavedItemHttpMapper } from '../Mapping/Http/SavedItemHttpMapper'
+import { ItemConflictHttpRepresentation } from '../Mapping/Http/ItemConflictHttpRepresentation'
+import { ItemConflictHttpMapper } from '../Mapping/Http/ItemConflictHttpMapper'
+import { ItemBackupRepresentation } from '../Mapping/Backup/ItemBackupRepresentation'
+import { ItemBackupMapper } from '../Mapping/Backup/ItemBackupMapper'
+import { SaveNewItem } from '../Domain/UseCase/Syncing/SaveNewItem/SaveNewItem'
+import { UpdateExistingItem } from '../Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
 
 
 export class ContainerConfigLoader {
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -122,6 +124,8 @@ export class ContainerConfigLoader {
 
 
     logger.debug('Database initialized')
     logger.debug('Database initialized')
 
 
+    container.bind<TimerInterface>(TYPES.Sync_Timer).toConstantValue(new Timer())
+
     const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
     const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
 
 
     container.bind<Env>(TYPES.Sync_Env).toConstantValue(env)
     container.bind<Env>(TYPES.Sync_Env).toConstantValue(env)
@@ -201,24 +205,37 @@ export class ContainerConfigLoader {
       })
       })
     }
     }
 
 
-    // Repositories
-    container.bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository).toDynamicValue((context: interfaces.Context) => {
-      return new TypeORMItemRepository(context.container.get(TYPES.Sync_ORMItemRepository))
-    })
+    // Mapping
+    container
+      .bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
+      .toConstantValue(new ItemPersistenceMapper())
+    container
+      .bind<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper)
+      .toConstantValue(new ItemHttpMapper(container.get(TYPES.Sync_Timer)))
+    container
+      .bind<MapperInterface<Item, SavedItemHttpRepresentation>>(TYPES.Sync_SavedItemHttpMapper)
+      .toConstantValue(new SavedItemHttpMapper(container.get(TYPES.Sync_Timer)))
+    container
+      .bind<MapperInterface<ItemConflict, ItemConflictHttpRepresentation>>(TYPES.Sync_ItemConflictHttpMapper)
+      .toConstantValue(new ItemConflictHttpMapper(container.get(TYPES.Sync_ItemHttpMapper)))
+    container
+      .bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
+      .toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
 
 
     // ORM
     // ORM
     container
     container
-      .bind<Repository<Item>>(TYPES.Sync_ORMItemRepository)
-      .toDynamicValue(() => appDataSource.getRepository(Item))
+      .bind<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository)
+      .toDynamicValue(() => appDataSource.getRepository(TypeORMItem))
 
 
-    // Projectors
+    // Repositories
     container
     container
-      .bind<ProjectorInterface<Item, ItemProjection>>(TYPES.Sync_ItemProjector)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new ItemProjector(context.container.get(TYPES.Sync_Timer))
-      })
-
-    container.bind<TimerInterface>(TYPES.Sync_Timer).toDynamicValue(() => new Timer())
+      .bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository)
+      .toConstantValue(
+        new TypeORMItemRepository(
+          container.get(TYPES.Sync_ORMItemRepository),
+          container.get(TYPES.Sync_ItemPersistenceMapper),
+        ),
+      )
 
 
     container
     container
       .bind<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory)
       .bind<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory)
@@ -245,18 +262,6 @@ export class ContainerConfigLoader {
         )
         )
       })
       })
 
 
-    // Projectors
-    container
-      .bind<ProjectorInterface<Item, SavedItemProjection>>(TYPES.Sync_SavedItemProjector)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new SavedItemProjector(context.container.get(TYPES.Sync_Timer))
-      })
-    container
-      .bind<ProjectorInterface<ItemConflict, ItemConflictProjection>>(TYPES.Sync_ItemConflictProjector)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new ItemConflictProjector(context.container.get(TYPES.Sync_ItemProjector))
-      })
-
     // env vars
     // env vars
     container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
     container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
     container
     container
@@ -287,60 +292,35 @@ export class ContainerConfigLoader {
     container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
     container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
       return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
       return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
     })
     })
-
-    // Services
-    container.bind<ItemServiceInterface>(TYPES.Sync_ItemService).toDynamicValue((context: interfaces.Context) => {
-      return new ItemService(
-        context.container.get(TYPES.Sync_ItemSaveValidator),
-        context.container.get(TYPES.Sync_ItemFactory),
-        context.container.get(TYPES.Sync_ItemRepository),
-        context.container.get(TYPES.Sync_DomainEventPublisher),
-        context.container.get(TYPES.Sync_DomainEventFactory),
-        context.container.get(TYPES.Sync_REVISIONS_FREQUENCY),
-        context.container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
-        context.container.get(TYPES.Sync_ItemTransferCalculator),
-        context.container.get(TYPES.Sync_Timer),
-        context.container.get(TYPES.Sync_ItemProjector),
-        context.container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
-        context.container.get(TYPES.Sync_Logger),
-      )
-    })
     container
     container
-      .bind<SyncResponseFactory20161215>(TYPES.Sync_SyncResponseFactory20161215)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new SyncResponseFactory20161215(context.container.get(TYPES.Sync_ItemProjector))
-      })
-    container
-      .bind<SyncResponseFactory20200115>(TYPES.Sync_SyncResponseFactory20200115)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new SyncResponseFactory20200115(
-          context.container.get(TYPES.Sync_ItemProjector),
-          context.container.get(TYPES.Sync_ItemConflictProjector),
-          context.container.get(TYPES.Sync_SavedItemProjector),
-        )
-      })
+      .bind<SaveNewItem>(TYPES.Sync_SaveNewItem)
+      .toConstantValue(
+        new SaveNewItem(
+          container.get(TYPES.Sync_ItemRepository),
+          container.get(TYPES.Sync_Timer),
+          container.get(TYPES.Sync_DomainEventPublisher),
+          container.get(TYPES.Sync_DomainEventFactory),
+        ),
+      )
     container
     container
-      .bind<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new SyncResponseFactoryResolver(
-          context.container.get(TYPES.Sync_SyncResponseFactory20161215),
-          context.container.get(TYPES.Sync_SyncResponseFactory20200115),
-        )
-      })
-
-    container.bind<ItemFactoryInterface>(TYPES.Sync_ItemFactory).toDynamicValue((context: interfaces.Context) => {
-      return new ItemFactory(context.container.get(TYPES.Sync_Timer), context.container.get(TYPES.Sync_ItemProjector))
-    })
+      .bind<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem)
+      .toConstantValue(
+        new UpdateExistingItem(
+          container.get(TYPES.Sync_ItemRepository),
+          container.get(TYPES.Sync_Timer),
+          container.get(TYPES.Sync_DomainEventPublisher),
+          container.get(TYPES.Sync_DomainEventFactory),
+          container.get(TYPES.Sync_REVISIONS_FREQUENCY),
+        ),
+      )
 
 
-    container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toDynamicValue(() => new OwnershipFilter())
+    // Services
+    container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toConstantValue(new OwnershipFilter())
     container
     container
       .bind<TimeDifferenceFilter>(TYPES.Sync_TimeDifferenceFilter)
       .bind<TimeDifferenceFilter>(TYPES.Sync_TimeDifferenceFilter)
-      .toDynamicValue(
-        (context: interfaces.Context) => new TimeDifferenceFilter(context.container.get(TYPES.Sync_Timer)),
-      )
-    container.bind<UuidFilter>(TYPES.Sync_UuidFilter).toDynamicValue(() => new UuidFilter())
-    container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toDynamicValue(() => new ContentTypeFilter())
-    container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toDynamicValue(() => new ContentFilter())
+      .toConstantValue(new TimeDifferenceFilter(container.get(TYPES.Sync_Timer)))
+    container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toConstantValue(new ContentTypeFilter())
+    container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toConstantValue(new ContentFilter())
 
 
     container
     container
       .bind<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
       .bind<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
@@ -348,12 +328,47 @@ export class ContainerConfigLoader {
         return new ItemSaveValidator([
         return new ItemSaveValidator([
           context.container.get(TYPES.Sync_OwnershipFilter),
           context.container.get(TYPES.Sync_OwnershipFilter),
           context.container.get(TYPES.Sync_TimeDifferenceFilter),
           context.container.get(TYPES.Sync_TimeDifferenceFilter),
-          context.container.get(TYPES.Sync_UuidFilter),
           context.container.get(TYPES.Sync_ContentTypeFilter),
           context.container.get(TYPES.Sync_ContentTypeFilter),
           context.container.get(TYPES.Sync_ContentFilter),
           context.container.get(TYPES.Sync_ContentFilter),
         ])
         ])
       })
       })
 
 
+    container
+      .bind<ItemServiceInterface>(TYPES.Sync_ItemService)
+      .toConstantValue(
+        new ItemService(
+          container.get(TYPES.Sync_ItemSaveValidator),
+          container.get(TYPES.Sync_ItemRepository),
+          container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
+          container.get(TYPES.Sync_ItemTransferCalculator),
+          container.get(TYPES.Sync_Timer),
+          container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
+          container.get(TYPES.Sync_SaveNewItem),
+          container.get(TYPES.Sync_UpdateExistingItem),
+          container.get(TYPES.Sync_Logger),
+        ),
+      )
+    container
+      .bind<SyncResponseFactory20161215>(TYPES.Sync_SyncResponseFactory20161215)
+      .toConstantValue(new SyncResponseFactory20161215(container.get(TYPES.Sync_ItemHttpMapper)))
+    container
+      .bind<SyncResponseFactory20200115>(TYPES.Sync_SyncResponseFactory20200115)
+      .toConstantValue(
+        new SyncResponseFactory20200115(
+          container.get(TYPES.Sync_ItemHttpMapper),
+          container.get(TYPES.Sync_ItemConflictHttpMapper),
+          container.get(TYPES.Sync_SavedItemHttpMapper),
+        ),
+      )
+    container
+      .bind<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver)
+      .toDynamicValue((context: interfaces.Context) => {
+        return new SyncResponseFactoryResolver(
+          context.container.get(TYPES.Sync_SyncResponseFactory20161215),
+          context.container.get(TYPES.Sync_SyncResponseFactory20200115),
+        )
+      })
+
     // env vars
     // env vars
     container
     container
       .bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
       .bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
@@ -421,14 +436,15 @@ export class ContainerConfigLoader {
         if (env.get('S3_AWS_REGION', true)) {
         if (env.get('S3_AWS_REGION', true)) {
           return new S3ItemBackupService(
           return new S3ItemBackupService(
             context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
             context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
-            context.container.get(TYPES.Sync_ItemProjector),
+            context.container.get(TYPES.Sync_ItemBackupMapper),
+            context.container.get(TYPES.Sync_ItemHttpMapper),
             context.container.get(TYPES.Sync_Logger),
             context.container.get(TYPES.Sync_Logger),
             context.container.get(TYPES.Sync_S3),
             context.container.get(TYPES.Sync_S3),
           )
           )
         } else {
         } else {
           return new FSItemBackupService(
           return new FSItemBackupService(
             context.container.get(TYPES.Sync_FILE_UPLOAD_PATH),
             context.container.get(TYPES.Sync_FILE_UPLOAD_PATH),
-            context.container.get(TYPES.Sync_ItemProjector),
+            context.container.get(TYPES.Sync_ItemBackupMapper),
             context.container.get(TYPES.Sync_Logger),
             context.container.get(TYPES.Sync_Logger),
           )
           )
         }
         }
@@ -512,7 +528,7 @@ export class ContainerConfigLoader {
             container.get(TYPES.Sync_SyncItems),
             container.get(TYPES.Sync_SyncItems),
             container.get(TYPES.Sync_CheckIntegrity),
             container.get(TYPES.Sync_CheckIntegrity),
             container.get(TYPES.Sync_GetItem),
             container.get(TYPES.Sync_GetItem),
-            container.get(TYPES.Sync_ItemProjector),
+            container.get(TYPES.Sync_ItemHttpMapper),
             container.get(TYPES.Sync_SyncResponseFactoryResolver),
             container.get(TYPES.Sync_SyncResponseFactoryResolver),
             container.get(TYPES.Sync_ControllerContainer),
             container.get(TYPES.Sync_ControllerContainer),
           ),
           ),

+ 3 - 3
packages/syncing-server/src/Bootstrap/DataSource.ts

@@ -1,9 +1,9 @@
 import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } from 'typeorm'
 import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } from 'typeorm'
 import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
 import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
-import { Item } from '../Domain/Item/Item'
-import { Notification } from '../Domain/Notifications/Notification'
 import { Env } from './Env'
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
+import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
+import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
 
 
 export class AppDataSource {
 export class AppDataSource {
   private _dataSource: DataSource | undefined
   private _dataSource: DataSource | undefined
@@ -33,7 +33,7 @@ export class AppDataSource {
 
 
     const commonDataSourceOptions = {
     const commonDataSourceOptions = {
       maxQueryExecutionTime,
       maxQueryExecutionTime,
-      entities: [Item, Notification],
+      entities: [TypeORMItem, TypeORMNotification],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrationsRun: true,
       migrationsRun: true,
       logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
       logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',

+ 7 - 6
packages/syncing-server/src/Bootstrap/Types.ts

@@ -12,10 +12,6 @@ const TYPES = {
   Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
   Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
   // Middleware
   // Middleware
   Sync_AuthMiddleware: Symbol.for('Sync_AuthMiddleware'),
   Sync_AuthMiddleware: Symbol.for('Sync_AuthMiddleware'),
-  // Projectors
-  Sync_ItemProjector: Symbol.for('Sync_ItemProjector'),
-  Sync_SavedItemProjector: Symbol.for('Sync_SavedItemProjector'),
-  Sync_ItemConflictProjector: Symbol.for('Sync_ItemConflictProjector'),
   // env vars
   // env vars
   Sync_REDIS_URL: Symbol.for('Sync_REDIS_URL'),
   Sync_REDIS_URL: Symbol.for('Sync_REDIS_URL'),
   Sync_SNS_TOPIC_ARN: Symbol.for('Sync_SNS_TOPIC_ARN'),
   Sync_SNS_TOPIC_ARN: Symbol.for('Sync_SNS_TOPIC_ARN'),
@@ -57,6 +53,8 @@ const TYPES = {
   Sync_SendMessageToUser: Symbol.for('Sync_SendMessageToUser'),
   Sync_SendMessageToUser: Symbol.for('Sync_SendMessageToUser'),
   Sync_DeleteAllMessagesSentToUser: Symbol.for('Sync_DeleteAllMessagesSentToUser'),
   Sync_DeleteAllMessagesSentToUser: Symbol.for('Sync_DeleteAllMessagesSentToUser'),
   Sync_DeleteMessage: Symbol.for('Sync_DeleteMessage'),
   Sync_DeleteMessage: Symbol.for('Sync_DeleteMessage'),
+  Sync_SaveNewItem: Symbol.for('Sync_SaveNewItem'),
+  Sync_UpdateExistingItem: Symbol.for('Sync_UpdateExistingItem'),
   // Handlers
   // Handlers
   Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
   Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
   Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
   Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
@@ -80,10 +78,8 @@ const TYPES = {
   Sync_ItemSaveValidator: Symbol.for('Sync_ItemSaveValidator'),
   Sync_ItemSaveValidator: Symbol.for('Sync_ItemSaveValidator'),
   Sync_OwnershipFilter: Symbol.for('Sync_OwnershipFilter'),
   Sync_OwnershipFilter: Symbol.for('Sync_OwnershipFilter'),
   Sync_TimeDifferenceFilter: Symbol.for('Sync_TimeDifferenceFilter'),
   Sync_TimeDifferenceFilter: Symbol.for('Sync_TimeDifferenceFilter'),
-  Sync_UuidFilter: Symbol.for('Sync_UuidFilter'),
   Sync_ContentTypeFilter: Symbol.for('Sync_ContentTypeFilter'),
   Sync_ContentTypeFilter: Symbol.for('Sync_ContentTypeFilter'),
   Sync_ContentFilter: Symbol.for('Sync_ContentFilter'),
   Sync_ContentFilter: Symbol.for('Sync_ContentFilter'),
-  Sync_ItemFactory: Symbol.for('Sync_ItemFactory'),
   Sync_ItemTransferCalculator: Symbol.for('Sync_ItemTransferCalculator'),
   Sync_ItemTransferCalculator: Symbol.for('Sync_ItemTransferCalculator'),
   Sync_ControllerContainer: Symbol.for('Sync_ControllerContainer'),
   Sync_ControllerContainer: Symbol.for('Sync_ControllerContainer'),
   Sync_HomeServerItemsController: Symbol.for('Sync_HomeServerItemsController'),
   Sync_HomeServerItemsController: Symbol.for('Sync_HomeServerItemsController'),
@@ -92,6 +88,11 @@ const TYPES = {
   Sync_SharedVaultUserHttpMapper: Symbol.for('Sync_SharedVaultUserHttpMapper'),
   Sync_SharedVaultUserHttpMapper: Symbol.for('Sync_SharedVaultUserHttpMapper'),
   Sync_SharedVaultInviteHttpMapper: Symbol.for('Sync_SharedVaultInviteHttpMapper'),
   Sync_SharedVaultInviteHttpMapper: Symbol.for('Sync_SharedVaultInviteHttpMapper'),
   Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
   Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
+  Sync_ItemPersistenceMapper: Symbol.for('Sync_ItemPersistenceMapper'),
+  Sync_ItemHttpMapper: Symbol.for('Sync_ItemHttpMapper'),
+  Sync_SavedItemHttpMapper: Symbol.for('Sync_SavedItemHttpMapper'),
+  Sync_ItemConflictHttpMapper: Symbol.for('Sync_ItemConflictHttpMapper'),
+  Sync_ItemBackupMapper: Symbol.for('Sync_ItemBackupMapper'),
 }
 }
 
 
 export default TYPES
 export default TYPES

+ 17 - 3
packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.spec.ts

@@ -9,6 +9,7 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { ExtensionsHttpService } from './ExtensionsHttpService'
 import { ExtensionsHttpService } from './ExtensionsHttpService'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { AxiosInstance } from 'axios'
 import { AxiosInstance } from 'axios'
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
 
 
 describe('ExtensionsHttpService', () => {
 describe('ExtensionsHttpService', () => {
   let httpClient: AxiosInstance
   let httpClient: AxiosInstance
@@ -34,9 +35,22 @@ describe('ExtensionsHttpService', () => {
     httpClient = {} as jest.Mocked<AxiosInstance>
     httpClient = {} as jest.Mocked<AxiosInstance>
     httpClient.request = jest.fn().mockReturnValue({ status: 200, data: { foo: 'bar' } })
     httpClient.request = jest.fn().mockReturnValue({ status: 200, data: { foo: 'bar' } })
 
 
-    item = {
-      content: 'test',
-    } as jest.Mocked<Item>
+    item = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
 
 
     authParams = {} as jest.Mocked<KeyParamsData>
     authParams = {} as jest.Mocked<KeyParamsData>
 
 

+ 2 - 2
packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.ts

@@ -140,11 +140,11 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
     email: string,
     email: string,
   ): Promise<DomainEventInterface> {
   ): Promise<DomainEventInterface> {
     const extension = await this.itemRepository.findByUuidAndUserUuid(extensionId, userUuid)
     const extension = await this.itemRepository.findByUuidAndUserUuid(extensionId, userUuid)
-    if (extension === null || !extension.content) {
+    if (extension === null || !extension.props.content) {
       throw Error(`Could not find extensions with id ${extensionId}`)
       throw Error(`Could not find extensions with id ${extensionId}`)
     }
     }
 
 
-    const content = this.contentDecoder.decode(extension.content)
+    const content = this.contentDecoder.decode(extension.props.content)
     switch (this.getExtensionName(content)) {
     switch (this.getExtensionName(content)) {
       case ExtensionName.Dropbox:
       case ExtensionName.Dropbox:
         return this.createCloudBackupFailedEventBasedOnProvider('DROPBOX', email)
         return this.createCloudBackupFailedEventBasedOnProvider('DROPBOX', email)

+ 17 - 4
packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts

@@ -5,6 +5,7 @@ import { Logger } from 'winston'
 import { Item } from '../Item/Item'
 import { Item } from '../Item/Item'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
 import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
 
 
 describe('AccountDeletionRequestedEventHandler', () => {
 describe('AccountDeletionRequestedEventHandler', () => {
   let itemRepository: ItemRepositoryInterface
   let itemRepository: ItemRepositoryInterface
@@ -15,10 +16,22 @@ describe('AccountDeletionRequestedEventHandler', () => {
   const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepository, logger)
   const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepository, logger)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    item = {
-      uuid: '1-2-3',
-      content: 'test',
-    } as jest.Mocked<Item>
+    item = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
 
 
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository.findAll = jest.fn().mockReturnValue([item])
     itemRepository.findAll = jest.fn().mockReturnValue([item])

+ 35 - 9
packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts

@@ -10,6 +10,7 @@ import { Item } from '../Item/Item'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { DuplicateItemSyncedEventHandler } from './DuplicateItemSyncedEventHandler'
 import { DuplicateItemSyncedEventHandler } from './DuplicateItemSyncedEventHandler'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
 
 
 describe('DuplicateItemSyncedEventHandler', () => {
 describe('DuplicateItemSyncedEventHandler', () => {
   let itemRepository: ItemRepositoryInterface
   let itemRepository: ItemRepositoryInterface
@@ -24,14 +25,39 @@ describe('DuplicateItemSyncedEventHandler', () => {
     new DuplicateItemSyncedEventHandler(itemRepository, domainEventFactory, domainEventPublisher, logger)
     new DuplicateItemSyncedEventHandler(itemRepository, domainEventFactory, domainEventPublisher, logger)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    originalItem = {
-      uuid: '1-2-3',
-    } as jest.Mocked<Item>
-
-    duplicateItem = {
-      uuid: '2-3-4',
-      duplicateOf: '1-2-3',
-    } as jest.Mocked<Item>
+    originalItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
+
+    duplicateItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
+    ).getValue()
 
 
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository.findByUuidAndUserUuid = jest
     itemRepository.findByUuidAndUserUuid = jest
@@ -81,7 +107,7 @@ describe('DuplicateItemSyncedEventHandler', () => {
   })
   })
 
 
   it('should not copy revisions if duplicate item is not pointing to duplicate anything', async () => {
   it('should not copy revisions if duplicate item is not pointing to duplicate anything', async () => {
-    duplicateItem.duplicateOf = null
+    duplicateItem.props.duplicateOf = null
     await createHandler().handle(event)
     await createHandler().handle(event)
 
 
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()

+ 4 - 4
packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts

@@ -24,22 +24,22 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
       return
       return
     }
     }
 
 
-    if (!item.duplicateOf) {
+    if (!item.props.duplicateOf) {
       this.logger.warn(`Item ${event.payload.itemUuid} does not point to any duplicate`)
       this.logger.warn(`Item ${event.payload.itemUuid} does not point to any duplicate`)
 
 
       return
       return
     }
     }
 
 
     const existingOriginalItem = await this.itemRepository.findByUuidAndUserUuid(
     const existingOriginalItem = await this.itemRepository.findByUuidAndUserUuid(
-      item.duplicateOf,
+      item.props.duplicateOf.value,
       event.payload.userUuid,
       event.payload.userUuid,
     )
     )
 
 
     if (existingOriginalItem !== null) {
     if (existingOriginalItem !== null) {
       await this.domainEventPublisher.publish(
       await this.domainEventPublisher.publish(
         this.domainEventFactory.createRevisionsCopyRequestedEvent(event.payload.userUuid, {
         this.domainEventFactory.createRevisionsCopyRequestedEvent(event.payload.userUuid, {
-          originalItemUuid: existingOriginalItem.uuid,
-          newItemUuid: item.uuid,
+          originalItemUuid: existingOriginalItem.id.toString(),
+          newItemUuid: item.id.toString(),
         }),
         }),
       )
       )
     }
     }

+ 17 - 4
packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts

@@ -11,6 +11,7 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { ItemRevisionCreationRequestedEventHandler } from './ItemRevisionCreationRequestedEventHandler'
 import { ItemRevisionCreationRequestedEventHandler } from './ItemRevisionCreationRequestedEventHandler'
 import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
 import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
 
 
 describe('ItemRevisionCreationRequestedEventHandler', () => {
 describe('ItemRevisionCreationRequestedEventHandler', () => {
   let itemRepository: ItemRepositoryInterface
   let itemRepository: ItemRepositoryInterface
@@ -29,10 +30,22 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
     )
     )
 
 
   beforeEach(() => {
   beforeEach(() => {
-    item = {
-      uuid: '1-2-3',
-      content: 'test',
-    } as jest.Mocked<Item>
+    item = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar1',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
 
 
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository.findByUuid = jest.fn().mockReturnValue(item)
     itemRepository.findByUuid = jest.fn().mockReturnValue(item)

+ 1 - 2
packages/syncing-server/src/Domain/Item/ExtendedIntegrityPayload.ts

@@ -1,6 +1,5 @@
-import { ContentType } from '@standardnotes/common'
 import { IntegrityPayload } from '@standardnotes/responses'
 import { IntegrityPayload } from '@standardnotes/responses'
 
 
 export type ExtendedIntegrityPayload = IntegrityPayload & {
 export type ExtendedIntegrityPayload = IntegrityPayload & {
-  content_type: ContentType
+  content_type: string | null
 }
 }

+ 16 - 113
packages/syncing-server/src/Domain/Item/Item.ts

@@ -1,119 +1,22 @@
-import { ContentType } from '@standardnotes/common'
-import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
 
 
-@Entity({ name: 'items' })
-@Index('index_items_on_user_uuid_and_content_type', ['userUuid', 'contentType'])
-@Index('user_uuid_and_updated_at_timestamp_and_created_at_timestamp', [
-  'userUuid',
-  'updatedAtTimestamp',
-  'createdAtTimestamp',
-])
-@Index('user_uuid_and_deleted', ['userUuid', 'deleted'])
-export class Item {
-  @PrimaryGeneratedColumn('uuid')
-  declare uuid: string
+import { ItemProps } from './ItemProps'
 
 
-  @Column({
-    type: 'varchar',
-    name: 'duplicate_of',
-    length: 36,
-    nullable: true,
-  })
-  declare duplicateOf: string | null
+export class Item extends Entity<ItemProps> {
+  get id(): UniqueEntityId {
+    return this._id
+  }
 
 
-  @Column({
-    type: 'varchar',
-    name: 'items_key_id',
-    length: 255,
-    nullable: true,
-  })
-  declare itemsKeyId: string | null
+  private constructor(props: ItemProps, id?: UniqueEntityId) {
+    super(props, id)
+  }
 
 
-  @Column({
-    type: 'text',
-    nullable: true,
-  })
-  declare content: string | null
+  static create(props: ItemProps, id?: UniqueEntityId): Result<Item> {
+    if (!props.contentSize) {
+      const contentSize = Buffer.byteLength(JSON.stringify(props))
+      props.contentSize = contentSize
+    }
 
 
-  @Column({
-    name: 'content_type',
-    type: 'varchar',
-    length: 255,
-    nullable: true,
-  })
-  @Index('index_items_on_content_type')
-  declare contentType: ContentType | null
-
-  @Column({
-    name: 'content_size',
-    type: 'int',
-    nullable: true,
-  })
-  declare contentSize: number | null
-
-  @Column({
-    name: 'enc_item_key',
-    type: 'text',
-    nullable: true,
-  })
-  declare encItemKey: string | null
-
-  @Column({
-    name: 'auth_hash',
-    type: 'varchar',
-    length: 255,
-    nullable: true,
-  })
-  declare authHash: string | null
-
-  @Column({
-    name: 'user_uuid',
-    length: 36,
-  })
-  @Index('index_items_on_user_uuid')
-  declare userUuid: string
-
-  @Column({
-    type: 'tinyint',
-    precision: 1,
-    nullable: true,
-    default: 0,
-  })
-  @Index('index_items_on_deleted')
-  declare deleted: boolean
-
-  @Column({
-    name: 'created_at',
-    type: 'datetime',
-    precision: 6,
-  })
-  declare createdAt: Date
-
-  @Column({
-    name: 'updated_at',
-    type: 'datetime',
-    precision: 6,
-  })
-  declare updatedAt: Date
-
-  @Column({
-    name: 'created_at_timestamp',
-    type: 'bigint',
-  })
-  declare createdAtTimestamp: number
-
-  @Column({
-    name: 'updated_at_timestamp',
-    type: 'bigint',
-  })
-  @Index('updated_at_timestamp')
-  declare updatedAtTimestamp: number
-
-  @Column({
-    name: 'updated_with_session',
-    type: 'varchar',
-    length: 36,
-    nullable: true,
-  })
-  declare updatedWithSession: string | null
+    return Result.ok<Item>(new Item(props, id))
+  }
 }
 }

+ 0 - 198
packages/syncing-server/src/Domain/Item/ItemFactory.spec.ts

@@ -1,198 +0,0 @@
-import 'reflect-metadata'
-
-import { Timer, TimerInterface } from '@standardnotes/time'
-import { ContentType } from '@standardnotes/common'
-
-import { ItemFactory } from './ItemFactory'
-import { ItemHash } from './ItemHash'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { ItemProjection } from '../../Projection/ItemProjection'
-import { Item } from './Item'
-
-describe('ItemFactory', () => {
-  let timer: TimerInterface
-  let itemProjector: ProjectorInterface<Item, ItemProjection>
-  let timeHelper: Timer
-
-  const createFactory = () => new ItemFactory(timer, itemProjector)
-
-  beforeEach(() => {
-    timeHelper = new Timer()
-
-    timer = {} as jest.Mocked<TimerInterface>
-    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
-    timer.convertMicrosecondsToDate = jest
-      .fn()
-      .mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds))
-    timer.convertStringDateToMicroseconds = jest
-      .fn()
-      .mockImplementation((date: string) => timeHelper.convertStringDateToMicroseconds(date))
-    timer.convertStringDateToDate = jest
-      .fn()
-      .mockImplementation((date: string) => timeHelper.convertStringDateToDate(date))
-
-    itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
-    itemProjector.projectFull = jest.fn().mockReturnValue({
-      uuid: '1-2-3',
-      items_key_id: 'foobar',
-      duplicate_of: null,
-      enc_item_key: 'foobar',
-      content: 'foobar',
-      content_type: ContentType.Note,
-      auth_hash: 'foobar',
-      deleted: false,
-      created_at: '2022-09-01 10:00:00',
-      created_at_timestamp: 123123123123123,
-      updated_at: '2022-09-01 10:00:00',
-      updated_at_timestamp: 123123123123123,
-      updated_with_session: '2-4-5',
-    })
-  })
-
-  it('should create an item based on item hash', () => {
-    const itemHash = {
-      uuid: '1-2-3',
-    } as jest.Mocked<ItemHash>
-
-    const item = createFactory().create({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
-
-    expect(item).toEqual({
-      createdAtTimestamp: 1616164633241568,
-      createdAt: expect.any(Date),
-      updatedWithSession: '1-2-3',
-      updatedAt: expect.any(Date),
-      updatedAtTimestamp: 1616164633241568,
-      userUuid: 'a-b-c',
-      uuid: '1-2-3',
-      contentSize: 341,
-    })
-  })
-
-  it('should create a stub item based on item hash with update_at date and timestamps overwritten', () => {
-    const itemHash = {
-      uuid: '1-2-3',
-      updated_at: '2021-03-25T09:37:37.943Z',
-    } as jest.Mocked<ItemHash>
-
-    const item = createFactory().createStub({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
-
-    expect(item).toEqual({
-      createdAtTimestamp: 1616164633241568,
-      createdAt: expect.any(Date),
-      updatedWithSession: '1-2-3',
-      updatedAt: new Date('2021-03-25T09:37:37.943Z'),
-      updatedAtTimestamp: 1616665057943000,
-      userUuid: 'a-b-c',
-      uuid: '1-2-3',
-      content: null,
-      contentSize: 341,
-    })
-  })
-
-  it('should create a stub item based on item hash with update_at_timestamp date and timestamps overwritten', () => {
-    const itemHash = {
-      uuid: '1-2-3',
-      updated_at_timestamp: 1616164633241568,
-      content: 'foobar',
-    } as jest.Mocked<ItemHash>
-
-    const item = createFactory().createStub({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
-
-    expect(item).toEqual({
-      createdAtTimestamp: 1616164633241568,
-      createdAt: expect.any(Date),
-      updatedWithSession: '1-2-3',
-      updatedAt: new Date('2021-03-19T14:37:13.241Z'),
-      updatedAtTimestamp: 1616164633241568,
-      userUuid: 'a-b-c',
-      uuid: '1-2-3',
-      content: 'foobar',
-      contentSize: 341,
-    })
-  })
-
-  it('should create a stub item based on item hash without updated timestamps', () => {
-    const itemHash = {
-      uuid: '1-2-3',
-    } as jest.Mocked<ItemHash>
-
-    const item = createFactory().createStub({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
-
-    expect(item).toEqual({
-      createdAtTimestamp: 1616164633241568,
-      createdAt: expect.any(Date),
-      updatedWithSession: '1-2-3',
-      updatedAt: expect.any(Date),
-      updatedAtTimestamp: 1616164633241568,
-      userUuid: 'a-b-c',
-      uuid: '1-2-3',
-      content: null,
-      contentSize: 341,
-    })
-  })
-
-  it('should create an item based on item hash with all fields filled', () => {
-    const itemHash = {
-      uuid: '1-2-3',
-      content: 'asdqwe1',
-      content_type: ContentType.Note,
-      duplicate_of: '222',
-      auth_hash: 'aaa',
-      deleted: true,
-      enc_item_key: 'qweqwe1',
-      items_key_id: 'asdasd1',
-      created_at: timeHelper.formatDate(new Date(1616164633241), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'),
-      updated_at: timeHelper.formatDate(new Date(1616164633242), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'),
-    } as jest.Mocked<ItemHash>
-
-    const item = createFactory().create({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
-
-    expect(item).toEqual({
-      content: 'asdqwe1',
-      contentSize: 341,
-      contentType: 'Note',
-      createdAt: expect.any(Date),
-      updatedWithSession: '1-2-3',
-      createdAtTimestamp: 1616164633241000,
-      encItemKey: 'qweqwe1',
-      itemsKeyId: 'asdasd1',
-      authHash: 'aaa',
-      deleted: true,
-      duplicateOf: '222',
-      updatedAt: expect.any(Date),
-      updatedAtTimestamp: 1616164633241568,
-      userUuid: 'a-b-c',
-      uuid: '1-2-3',
-    })
-  })
-
-  it('should create an item based on item hash with created at timestamp', () => {
-    const itemHash = {
-      uuid: '1-2-3',
-      content: 'asdqwe1',
-      content_type: ContentType.Note,
-      duplicate_of: null,
-      enc_item_key: 'qweqwe1',
-      items_key_id: 'asdasd1',
-      created_at_timestamp: 1616164633241312,
-      updated_at: timeHelper.formatDate(new Date(1616164633242), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'),
-    } as jest.Mocked<ItemHash>
-
-    const item = createFactory().create({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
-
-    expect(item).toEqual({
-      content: 'asdqwe1',
-      contentSize: 341,
-      contentType: 'Note',
-      createdAt: expect.any(Date),
-      updatedWithSession: '1-2-3',
-      createdAtTimestamp: 1616164633241312,
-      encItemKey: 'qweqwe1',
-      itemsKeyId: 'asdasd1',
-      updatedAt: expect.any(Date),
-      updatedAtTimestamp: 1616164633241568,
-      userUuid: 'a-b-c',
-      uuid: '1-2-3',
-    })
-  })
-})

+ 0 - 79
packages/syncing-server/src/Domain/Item/ItemFactory.ts

@@ -1,79 +0,0 @@
-import { TimerInterface } from '@standardnotes/time'
-
-import { ItemProjection } from '../../Projection/ItemProjection'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { Item } from './Item'
-import { ItemFactoryInterface } from './ItemFactoryInterface'
-import { ItemHash } from './ItemHash'
-
-export class ItemFactory implements ItemFactoryInterface {
-  constructor(private timer: TimerInterface, private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
-
-  createStub(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item {
-    const item = this.create(dto)
-
-    if (dto.itemHash.content === undefined) {
-      item.content = null
-    }
-
-    if (dto.itemHash.updated_at_timestamp) {
-      item.updatedAtTimestamp = dto.itemHash.updated_at_timestamp
-      item.updatedAt = this.timer.convertMicrosecondsToDate(dto.itemHash.updated_at_timestamp)
-    } else if (dto.itemHash.updated_at) {
-      item.updatedAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.updated_at)
-      item.updatedAt = this.timer.convertStringDateToDate(dto.itemHash.updated_at)
-    }
-
-    return item
-  }
-
-  create(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item {
-    const newItem = new Item()
-    newItem.uuid = dto.itemHash.uuid
-    newItem.updatedWithSession = dto.sessionUuid
-    newItem.contentSize = 0
-    if (dto.itemHash.content) {
-      newItem.content = dto.itemHash.content
-    }
-    newItem.userUuid = dto.userUuid
-    if (dto.itemHash.content_type) {
-      newItem.contentType = dto.itemHash.content_type
-    }
-    if (dto.itemHash.enc_item_key) {
-      newItem.encItemKey = dto.itemHash.enc_item_key
-    }
-    if (dto.itemHash.items_key_id) {
-      newItem.itemsKeyId = dto.itemHash.items_key_id
-    }
-    if (dto.itemHash.duplicate_of) {
-      newItem.duplicateOf = dto.itemHash.duplicate_of
-    }
-    if (dto.itemHash.deleted !== undefined) {
-      newItem.deleted = dto.itemHash.deleted
-    }
-    if (dto.itemHash.auth_hash) {
-      newItem.authHash = dto.itemHash.auth_hash
-    }
-
-    const now = this.timer.getTimestampInMicroseconds()
-    const nowDate = this.timer.convertMicrosecondsToDate(now)
-
-    newItem.updatedAtTimestamp = now
-    newItem.updatedAt = nowDate
-
-    newItem.createdAtTimestamp = now
-    newItem.createdAt = nowDate
-
-    if (dto.itemHash.created_at_timestamp) {
-      newItem.createdAtTimestamp = dto.itemHash.created_at_timestamp
-      newItem.createdAt = this.timer.convertMicrosecondsToDate(dto.itemHash.created_at_timestamp)
-    } else if (dto.itemHash.created_at) {
-      newItem.createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
-      newItem.createdAt = this.timer.convertStringDateToDate(dto.itemHash.created_at)
-    }
-
-    newItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(newItem)))
-
-    return newItem
-  }
-}

+ 0 - 7
packages/syncing-server/src/Domain/Item/ItemFactoryInterface.ts

@@ -1,7 +0,0 @@
-import { Item } from './Item'
-import { ItemHash } from './ItemHash'
-
-export interface ItemFactoryInterface {
-  create(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item
-  createStub(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item
-}

+ 1 - 3
packages/syncing-server/src/Domain/Item/ItemHash.ts

@@ -1,9 +1,7 @@
-import { ContentType } from '@standardnotes/common'
-
 export type ItemHash = {
 export type ItemHash = {
   uuid: string
   uuid: string
   content?: string
   content?: string
-  content_type: ContentType
+  content_type: string | null
   deleted?: boolean
   deleted?: boolean
   duplicate_of?: string | null
   duplicate_of?: string | null
   auth_hash?: string
   auth_hash?: string

+ 16 - 0
packages/syncing-server/src/Domain/Item/ItemProps.ts

@@ -0,0 +1,16 @@
+import { ContentType, Dates, Timestamps, Uuid } from '@standardnotes/domain-core'
+
+export interface ItemProps {
+  duplicateOf: Uuid | null
+  itemsKeyId: string | null
+  content: string | null
+  contentType: ContentType
+  encItemKey: string | null
+  authHash: string | null
+  userUuid: Uuid
+  deleted: boolean
+  updatedWithSession: Uuid | null
+  dates: Dates
+  timestamps: Timestamps
+  contentSize?: number
+}

+ 2 - 2
packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts

@@ -16,8 +16,8 @@ export interface ItemRepositoryInterface {
   findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]>
   findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]>
   findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null>
   findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null>
   findByUuid(uuid: string): Promise<Item | null>
   findByUuid(uuid: string): Promise<Item | null>
-  remove(item: Item): Promise<Item>
-  save(item: Item): Promise<Item>
+  remove(item: Item): Promise<void>
+  save(item: Item): Promise<void>
   markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void>
   markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void>
   updateContentSize(itemUuid: string, contentSize: number): Promise<void>
   updateContentSize(itemUuid: string, contentSize: number): Promise<void>
 }
 }

+ 203 - 388
packages/syncing-server/src/Domain/Item/ItemService.spec.ts

@@ -1,94 +1,101 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
 
 
-import { ContentType } from '@standardnotes/common'
+import { Timer, TimerInterface } from '@standardnotes/time'
+import { Logger } from 'winston'
+
 import { Item } from './Item'
 import { Item } from './Item'
 import { ItemHash } from './ItemHash'
 import { ItemHash } from './ItemHash'
 
 
 import { ItemRepositoryInterface } from './ItemRepositoryInterface'
 import { ItemRepositoryInterface } from './ItemRepositoryInterface'
 import { ItemService } from './ItemService'
 import { ItemService } from './ItemService'
 import { ApiVersion } from '../Api/ApiVersion'
 import { ApiVersion } from '../Api/ApiVersion'
-import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
-import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
-import { Logger } from 'winston'
-import { Timer, TimerInterface } from '@standardnotes/time'
 import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
 import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
-import { ItemFactoryInterface } from './ItemFactoryInterface'
 import { ItemConflict } from './ItemConflict'
 import { ItemConflict } from './ItemConflict'
 import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
 import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { ItemProjection } from '../../Projection/ItemProjection'
+import { SaveNewItem } from '../UseCase/Syncing/SaveNewItem/SaveNewItem'
+import { UpdateExistingItem } from '../UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
 
 
 describe('ItemService', () => {
 describe('ItemService', () => {
   let itemRepository: ItemRepositoryInterface
   let itemRepository: ItemRepositoryInterface
-  let domainEventPublisher: DomainEventPublisherInterface
-  let domainEventFactory: DomainEventFactoryInterface
-  const revisionFrequency = 300
   const contentSizeTransferLimit = 100
   const contentSizeTransferLimit = 100
   let timer: TimerInterface
   let timer: TimerInterface
   let item1: Item
   let item1: Item
   let item2: Item
   let item2: Item
   let itemHash1: ItemHash
   let itemHash1: ItemHash
   let itemHash2: ItemHash
   let itemHash2: ItemHash
-  let emptyHash: ItemHash
   let syncToken: string
   let syncToken: string
   let logger: Logger
   let logger: Logger
   let itemSaveValidator: ItemSaveValidatorInterface
   let itemSaveValidator: ItemSaveValidatorInterface
   let newItem: Item
   let newItem: Item
-  let itemFactory: ItemFactoryInterface
   let timeHelper: Timer
   let timeHelper: Timer
   let itemTransferCalculator: ItemTransferCalculatorInterface
   let itemTransferCalculator: ItemTransferCalculatorInterface
-  let itemProjector: ProjectorInterface<Item, ItemProjection>
+  let saveNewItemUseCase: SaveNewItem
+  let updateExistingItemUseCase: UpdateExistingItem
   const maxItemsSyncLimit = 300
   const maxItemsSyncLimit = 300
 
 
   const createService = () =>
   const createService = () =>
     new ItemService(
     new ItemService(
       itemSaveValidator,
       itemSaveValidator,
-      itemFactory,
       itemRepository,
       itemRepository,
-      domainEventPublisher,
-      domainEventFactory,
-      revisionFrequency,
       contentSizeTransferLimit,
       contentSizeTransferLimit,
       itemTransferCalculator,
       itemTransferCalculator,
       timer,
       timer,
-      itemProjector,
       maxItemsSyncLimit,
       maxItemsSyncLimit,
+      saveNewItemUseCase,
+      updateExistingItemUseCase,
       logger,
       logger,
     )
     )
 
 
   beforeEach(() => {
   beforeEach(() => {
     timeHelper = new Timer()
     timeHelper = new Timer()
 
 
-    item1 = {
-      uuid: '1-2-3',
-      userUuid: '1-2-3',
-      createdAt: new Date(1616164633241311),
-      createdAtTimestamp: 1616164633241311,
-      updatedAt: new Date(1616164633241311),
-      updatedAtTimestamp: 1616164633241311,
-    } as jest.Mocked<Item>
-    item2 = {
-      uuid: '2-3-4',
-      userUuid: '1-2-3',
-      createdAt: new Date(1616164633241312),
-      createdAtTimestamp: 1616164633241312,
-      updatedAt: new Date(1616164633241312),
-      updatedAtTimestamp: 1616164633241312,
-    } as jest.Mocked<Item>
+    item1 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar1',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
+    item2 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar2',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
+        timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
+    ).getValue()
 
 
     itemHash1 = {
     itemHash1 = {
       uuid: '1-2-3',
       uuid: '1-2-3',
       content: 'asdqwe1',
       content: 'asdqwe1',
-      content_type: ContentType.Note,
+      content_type: ContentType.TYPES.Note,
       duplicate_of: null,
       duplicate_of: null,
       enc_item_key: 'qweqwe1',
       enc_item_key: 'qweqwe1',
       items_key_id: 'asdasd1',
       items_key_id: 'asdasd1',
       created_at: timeHelper.formatDate(
       created_at: timeHelper.formatDate(
-        timeHelper.convertMicrosecondsToDate(item1.createdAtTimestamp),
+        timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       ),
       ),
       updated_at: timeHelper.formatDate(
       updated_at: timeHelper.formatDate(
-        new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.updatedAtTimestamp) + 1),
+        new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       ),
       ),
     } as jest.Mocked<ItemHash>
     } as jest.Mocked<ItemHash>
@@ -96,31 +103,28 @@ describe('ItemService', () => {
     itemHash2 = {
     itemHash2 = {
       uuid: '2-3-4',
       uuid: '2-3-4',
       content: 'asdqwe2',
       content: 'asdqwe2',
-      content_type: ContentType.Note,
+      content_type: ContentType.TYPES.Note,
       duplicate_of: null,
       duplicate_of: null,
       enc_item_key: 'qweqwe2',
       enc_item_key: 'qweqwe2',
       items_key_id: 'asdasd2',
       items_key_id: 'asdasd2',
       created_at: timeHelper.formatDate(
       created_at: timeHelper.formatDate(
-        timeHelper.convertMicrosecondsToDate(item2.createdAtTimestamp),
+        timeHelper.convertMicrosecondsToDate(item2.props.timestamps.createdAt),
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       ),
       ),
       updated_at: timeHelper.formatDate(
       updated_at: timeHelper.formatDate(
-        new Date(timeHelper.convertMicrosecondsToMilliseconds(item2.updatedAtTimestamp) + 1),
+        new Date(timeHelper.convertMicrosecondsToMilliseconds(item2.props.timestamps.updatedAt) + 1),
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       ),
       ),
     } as jest.Mocked<ItemHash>
     } as jest.Mocked<ItemHash>
 
 
-    emptyHash = {
-      uuid: '2-3-4',
-    } as jest.Mocked<ItemHash>
-
     itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
     itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
-    itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockReturnValue([item1.uuid, item2.uuid])
+    itemTransferCalculator.computeItemUuidsToFetch = jest
+      .fn()
+      .mockReturnValue([item1.id.toString(), item2.id.toString()])
 
 
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
     itemRepository.findAll = jest.fn().mockReturnValue([item1, item2])
     itemRepository.findAll = jest.fn().mockReturnValue([item1, item2])
     itemRepository.countAll = jest.fn().mockReturnValue(2)
     itemRepository.countAll = jest.fn().mockReturnValue(2)
-    itemRepository.save = jest.fn().mockImplementation((item: Item) => item)
 
 
     timer = {} as jest.Mocked<TimerInterface>
     timer = {} as jest.Mocked<TimerInterface>
     timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
     timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
@@ -136,13 +140,6 @@ describe('ItemService', () => {
       .fn()
       .fn()
       .mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds))
       .mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds))
 
 
-    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
-    domainEventPublisher.publish = jest.fn()
-
-    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
-    domainEventFactory.createDuplicateItemSyncedEvent = jest.fn()
-    domainEventFactory.createItemRevisionCreationRequested = jest.fn()
-
     logger = {} as jest.Mocked<Logger>
     logger = {} as jest.Mocked<Logger>
     logger.error = jest.fn()
     logger.error = jest.fn()
     logger.warn = jest.fn()
     logger.warn = jest.fn()
@@ -152,31 +149,28 @@ describe('ItemService', () => {
     itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
     itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
     itemSaveValidator.validate = jest.fn().mockReturnValue({ passed: true })
     itemSaveValidator.validate = jest.fn().mockReturnValue({ passed: true })
 
 
-    newItem = {
-      contentType: ContentType.Note,
-    } as jest.Mocked<Item>
-
-    itemFactory = {} as jest.Mocked<ItemFactoryInterface>
-    itemFactory.create = jest.fn().mockReturnValue(newItem)
-    itemFactory.createStub = jest.fn().mockReturnValue(newItem)
-
-    itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
-    itemProjector.projectFull = jest.fn().mockReturnValue({
-      uuid: '1-2-3',
-      items_key_id: 'foobar',
-      duplicate_of: null,
-      enc_item_key: 'foobar',
-      content:
-        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed viverra tellus in hac habitasse. Tortor posuere ac ut consequat semper. Ut diam quam nulla porttitor. Sapien pellentesque habitant morbi tristique senectus et netus et malesuada. Dapibus ultrices in iaculis nunc. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Faucibus et molestie ac feugiat sed lectus vestibulum mattis. Eu consequat ac felis donec. Eget velit aliquet sagittis id. Nullam eget felis eget nunc. Turpis in eu mi bibendum neque egestas congue.',
-      content_type: ContentType.Note,
-      auth_hash: 'foobar',
-      deleted: false,
-      created_at: '2022-09-01 10:00:00',
-      created_at_timestamp: 123123123123123,
-      updated_at: '2022-09-01 10:00:00',
-      updated_at_timestamp: 123123123123123,
-      updated_with_session: '2-4-5',
-    })
+    newItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar2',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241313), new Date(1616164633241313)).getValue(),
+        timestamps: Timestamps.create(1616164633241313, 1616164633241313).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
+    ).getValue()
+
+    saveNewItemUseCase = {} as jest.Mocked<SaveNewItem>
+    saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(newItem))
+
+    updateExistingItemUseCase = {} as jest.Mocked<UpdateExistingItem>
+    updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(item1))
   })
   })
 
 
   it('should retrieve all items for a user from last sync with sync token version 1', async () => {
   it('should retrieve all items for a user from last sync with sync token version 1', async () => {
@@ -186,7 +180,7 @@ describe('ItemService', () => {
       await createService().getItems({
       await createService().getItems({
         userUuid: '1-2-3',
         userUuid: '1-2-3',
         syncToken,
         syncToken,
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
       }),
       }),
     ).toEqual({
     ).toEqual({
       items: [item1, item2],
       items: [item1, item2],
@@ -202,7 +196,7 @@ describe('ItemService', () => {
       limit: 150,
       limit: 150,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortOrder: 'ASC',
       sortOrder: 'ASC',
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
     })
     })
@@ -213,7 +207,7 @@ describe('ItemService', () => {
       await createService().getItems({
       await createService().getItems({
         userUuid: '1-2-3',
         userUuid: '1-2-3',
         syncToken,
         syncToken,
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
       }),
       }),
     ).toEqual({
     ).toEqual({
       items: [item1, item2],
       items: [item1, item2],
@@ -229,7 +223,7 @@ describe('ItemService', () => {
       limit: 150,
       limit: 150,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
       sortOrder: 'ASC',
       sortOrder: 'ASC',
     })
     })
@@ -240,7 +234,7 @@ describe('ItemService', () => {
       await createService().getItems({
       await createService().getItems({
         userUuid: '1-2-3',
         userUuid: '1-2-3',
         syncToken,
         syncToken,
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
         limit: 1000,
         limit: 1000,
       }),
       }),
     ).toEqual({
     ).toEqual({
@@ -257,7 +251,7 @@ describe('ItemService', () => {
       limit: 300,
       limit: 300,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
       sortOrder: 'ASC',
       sortOrder: 'ASC',
     })
     })
@@ -270,7 +264,7 @@ describe('ItemService', () => {
       await createService().getItems({
       await createService().getItems({
         userUuid: '1-2-3',
         userUuid: '1-2-3',
         syncToken,
         syncToken,
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
       }),
       }),
     ).toEqual({
     ).toEqual({
       items: [],
       items: [],
@@ -295,7 +289,7 @@ describe('ItemService', () => {
       userUuid: '1-2-3',
       userUuid: '1-2-3',
       syncToken,
       syncToken,
       limit: 1,
       limit: 1,
-      contentType: ContentType.Note,
+      contentType: ContentType.TYPES.Note,
     })
     })
 
 
     expect(itemsResponse).toEqual({
     expect(itemsResponse).toEqual({
@@ -315,7 +309,7 @@ describe('ItemService', () => {
       limit: 1,
       limit: 1,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
       sortOrder: 'ASC',
       sortOrder: 'ASC',
     })
     })
@@ -329,7 +323,7 @@ describe('ItemService', () => {
         userUuid: '1-2-3',
         userUuid: '1-2-3',
         syncToken,
         syncToken,
         cursorToken,
         cursorToken,
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
       }),
       }),
     ).toEqual({
     ).toEqual({
       items: [item1, item2],
       items: [item1, item2],
@@ -345,7 +339,7 @@ describe('ItemService', () => {
       limit: 150,
       limit: 150,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
       sortOrder: 'ASC',
       sortOrder: 'ASC',
     })
     })
@@ -355,7 +349,7 @@ describe('ItemService', () => {
     expect(
     expect(
       await createService().getItems({
       await createService().getItems({
         userUuid: '1-2-3',
         userUuid: '1-2-3',
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
       }),
       }),
     ).toEqual({
     ).toEqual({
       items: [item1, item2],
       items: [item1, item2],
@@ -371,7 +365,7 @@ describe('ItemService', () => {
       limit: 150,
       limit: 150,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
       sortOrder: 'ASC',
       sortOrder: 'ASC',
     })
     })
@@ -381,7 +375,7 @@ describe('ItemService', () => {
     await createService().getItems({
     await createService().getItems({
       userUuid: '1-2-3',
       userUuid: '1-2-3',
       syncToken,
       syncToken,
-      contentType: ContentType.Note,
+      contentType: ContentType.TYPES.Note,
     })
     })
 
 
     expect(itemRepository.countAll).toHaveBeenCalledWith({
     expect(itemRepository.countAll).toHaveBeenCalledWith({
@@ -394,7 +388,7 @@ describe('ItemService', () => {
       limit: 150,
       limit: 150,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortOrder: 'ASC',
       sortOrder: 'ASC',
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
     })
     })
@@ -405,7 +399,7 @@ describe('ItemService', () => {
       userUuid: '1-2-3',
       userUuid: '1-2-3',
       syncToken,
       syncToken,
       limit: 0,
       limit: 0,
-      contentType: ContentType.Note,
+      contentType: ContentType.TYPES.Note,
     })
     })
 
 
     expect(itemRepository.countAll).toHaveBeenCalledWith({
     expect(itemRepository.countAll).toHaveBeenCalledWith({
@@ -418,7 +412,7 @@ describe('ItemService', () => {
       limit: 150,
       limit: 150,
     })
     })
     expect(itemRepository.findAll).toHaveBeenCalledWith({
     expect(itemRepository.findAll).toHaveBeenCalledWith({
-      uuids: ['1-2-3', '2-3-4'],
+      uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
       sortOrder: 'ASC',
       sortOrder: 'ASC',
     })
     })
@@ -432,7 +426,7 @@ describe('ItemService', () => {
         userUuid: '1-2-3',
         userUuid: '1-2-3',
         syncToken: '2:',
         syncToken: '2:',
         limit: 0,
         limit: 0,
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
       })
       })
     } catch (e) {
     } catch (e) {
       error = e
       error = e
@@ -449,7 +443,7 @@ describe('ItemService', () => {
         userUuid: '1-2-3',
         userUuid: '1-2-3',
         syncToken: '1234567890',
         syncToken: '1234567890',
         limit: 0,
         limit: 0,
-        contentType: ContentType.Note,
+        contentType: ContentType.TYPES.Note,
       })
       })
     } catch (e) {
     } catch (e) {
       error = e
       error = e
@@ -459,12 +453,38 @@ describe('ItemService', () => {
   })
   })
 
 
   it('should front load keys items to top of the collection for better client performance', async () => {
   it('should front load keys items to top of the collection for better client performance', async () => {
-    const item3 = {
-      uuid: '1-2-3',
-    } as jest.Mocked<Item>
-    const item4 = {
-      uuid: '4-5-6',
-    } as jest.Mocked<Item>
+    const item3 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar1',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
+    ).getValue()
+    const item4 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar2',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
+        timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000004'),
+    ).getValue()
 
 
     itemRepository.findAll = jest.fn().mockReturnValue([item3, item4])
     itemRepository.findAll = jest.fn().mockReturnValue([item3, item4])
 
 
@@ -485,11 +505,10 @@ describe('ItemService', () => {
     expect(result).toEqual({
     expect(result).toEqual({
       conflicts: [],
       conflicts: [],
       savedItems: [newItem],
       savedItems: [newItem],
-      syncToken: 'MjpOYU4=',
+      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxNA==',
     })
     })
 
 
-    expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalledTimes(1)
-    expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
+    expect(saveNewItemUseCase.execute).toHaveBeenCalled()
   })
   })
 
 
   it('should not save new items in read only access mode', async () => {
   it('should not save new items in read only access mode', async () => {
@@ -513,16 +532,29 @@ describe('ItemService', () => {
       savedItems: [],
       savedItems: [],
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
     })
     })
+
+    expect(saveNewItemUseCase.execute).not.toHaveBeenCalled()
   })
   })
 
 
   it('should save new items that are duplicates', async () => {
   it('should save new items that are duplicates', async () => {
     itemRepository.findByUuid = jest.fn().mockReturnValue(null)
     itemRepository.findByUuid = jest.fn().mockReturnValue(null)
-    const duplicateItem = {
-      updatedAtTimestamp: 1616164633241570,
-      duplicateOf: '1-2-3',
-      contentType: ContentType.Note,
-    } as jest.Mocked<Item>
-    itemFactory.create = jest.fn().mockReturnValueOnce(duplicateItem)
+    const duplicateItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar1',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241570), new Date(1616164633241570)).getValue(),
+        timestamps: Timestamps.create(1616164633241570, 1616164633241570).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000005'),
+    ).getValue()
+    saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(duplicateItem))
 
 
     const result = await createService().saveItems({
     const result = await createService().saveItems({
       itemHashes: [itemHash1],
       itemHashes: [itemHash1],
@@ -537,10 +569,6 @@ describe('ItemService', () => {
       savedItems: [duplicateItem],
       savedItems: [duplicateItem],
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU3MQ==',
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU3MQ==',
     })
     })
-
-    expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalledTimes(1)
-    expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
-    expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalledTimes(1)
   })
   })
 
 
   it('should skip items that are conflicting on validation', async () => {
   it('should skip items that are conflicting on validation', async () => {
@@ -568,7 +596,7 @@ describe('ItemService', () => {
   it('should mark items as saved that are skipped on validation', async () => {
   it('should mark items as saved that are skipped on validation', async () => {
     itemRepository.findByUuid = jest.fn().mockReturnValue(null)
     itemRepository.findByUuid = jest.fn().mockReturnValue(null)
 
 
-    const skipped = {} as jest.Mocked<Item>
+    const skipped = item1
     const validationResult = { passed: false, skipped }
     const validationResult = { passed: false, skipped }
     itemSaveValidator.validate = jest.fn().mockReturnValue(validationResult)
     itemSaveValidator.validate = jest.fn().mockReturnValue(validationResult)
 
 
@@ -583,7 +611,7 @@ describe('ItemService', () => {
     expect(result).toEqual({
     expect(result).toEqual({
       conflicts: [],
       conflicts: [],
       savedItems: [skipped],
       savedItems: [skipped],
-      syncToken: 'MjpOYU4=',
+      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMg==',
     })
     })
   })
   })
 
 
@@ -593,7 +621,7 @@ describe('ItemService', () => {
     const itemHash3 = {
     const itemHash3 = {
       uuid: '3-4-5',
       uuid: '3-4-5',
       content: 'asdqwe3',
       content: 'asdqwe3',
-      content_type: ContentType.Note,
+      content_type: ContentType.TYPES.Note,
       duplicate_of: null,
       duplicate_of: null,
       enc_item_key: 'qweqwe3',
       enc_item_key: 'qweqwe3',
       items_key_id: 'asdasd3',
       items_key_id: 'asdasd3',
@@ -607,11 +635,41 @@ describe('ItemService', () => {
     const item3Timestamp = 1616164633241569
     const item3Timestamp = 1616164633241569
     timer.getTimestampInMicroseconds = jest.fn().mockReturnValueOnce(saveProcedureStartTimestamp)
     timer.getTimestampInMicroseconds = jest.fn().mockReturnValueOnce(saveProcedureStartTimestamp)
 
 
-    itemFactory.create = jest
+    saveNewItemUseCase.execute = jest
       .fn()
       .fn()
-      .mockReturnValueOnce({ updatedAtTimestamp: item1Timestamp, duplicateOf: null } as jest.Mocked<Item>)
-      .mockReturnValueOnce({ updatedAtTimestamp: item2Timestamp, duplicateOf: null } as jest.Mocked<Item>)
-      .mockReturnValueOnce({ updatedAtTimestamp: item3Timestamp, duplicateOf: null } as jest.Mocked<Item>)
+      .mockReturnValueOnce(
+        Result.ok(
+          Item.create(
+            {
+              ...item1.props,
+              timestamps: Timestamps.create(item1Timestamp, item1Timestamp).getValue(),
+            },
+            new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
+          ).getValue(),
+        ),
+      )
+      .mockReturnValueOnce(
+        Result.ok(
+          Item.create(
+            {
+              ...item2.props,
+              timestamps: Timestamps.create(item2Timestamp, item2Timestamp).getValue(),
+            },
+            new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
+          ).getValue(),
+        ),
+      )
+      .mockReturnValueOnce(
+        Result.ok(
+          Item.create(
+            {
+              ...item2.props,
+              timestamps: Timestamps.create(item3Timestamp, item3Timestamp).getValue(),
+            },
+            new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
+          ).getValue(),
+        ),
+      )
 
 
     const result = await createService().saveItems({
     const result = await createService().saveItems({
       itemHashes: [itemHash1, itemHash3, itemHash2],
       itemHashes: [itemHash1, itemHash3, itemHash2],
@@ -638,66 +696,14 @@ describe('ItemService', () => {
 
 
     expect(result).toEqual({
     expect(result).toEqual({
       conflicts: [],
       conflicts: [],
-      savedItems: [
-        {
-          content: 'asdqwe1',
-          contentSize: 950,
-          contentType: 'Note',
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          encItemKey: 'qweqwe1',
-          itemsKeyId: 'asdasd1',
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
-        },
-      ],
-      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
-    })
-  })
-
-  it('should update existing items from legacy clients', async () => {
-    itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
-
-    delete itemHash1.updated_at
-    delete itemHash1.updated_at_timestamp
-
-    const result = await createService().saveItems({
-      itemHashes: [itemHash1],
-      userUuid: '1-2-3',
-      apiVersion: ApiVersion.v20161215,
-      readOnlyAccess: false,
-      sessionUuid: '2-3-4',
-    })
-
-    expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
-        {
-          content: 'asdqwe1',
-          contentSize: 950,
-          contentType: 'Note',
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          encItemKey: 'qweqwe1',
-          itemsKeyId: 'asdasd1',
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
-        },
-      ],
-      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
+      savedItems: [item1],
+      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMg==',
     })
     })
   })
   })
 
 
-  it('should update existing items with created_at_timestamp', async () => {
-    itemHash1.created_at_timestamp = 123
-    itemHash1.updated_at_timestamp = item1.updatedAtTimestamp
+  it('should mark as skipped existing items that failed to update', async () => {
     itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
     itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
+    updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
 
 
     const result = await createService().saveItems({
     const result = await createService().saveItems({
       itemHashes: [itemHash1],
       itemHashes: [itemHash1],
@@ -708,168 +714,23 @@ describe('ItemService', () => {
     })
     })
 
 
     expect(result).toEqual({
     expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
-        {
-          content: 'asdqwe1',
-          contentSize: 950,
-          contentType: 'Note',
-          createdAtTimestamp: 123,
-          createdAt: expect.any(Date),
-          encItemKey: 'qweqwe1',
-          itemsKeyId: 'asdasd1',
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
-        },
-      ],
-      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
-    })
-  })
-
-  it('should update existing empty hashes', async () => {
-    itemRepository.findByUuid = jest.fn().mockReturnValue(item2)
-    emptyHash.updated_at = timeHelper.formatDate(
-      new Date(timeHelper.convertMicrosecondsToMilliseconds(item2.updatedAtTimestamp) + 1),
-      'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
-    )
-
-    const result = await createService().saveItems({
-      itemHashes: [emptyHash],
-      userUuid: '1-2-3',
-      apiVersion: ApiVersion.v20200115,
-      readOnlyAccess: false,
-      sessionUuid: '2-3-4',
-    })
-
-    expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
-        {
-          contentSize: 950,
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '2-3-4',
-        },
-      ],
-      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
-    })
-  })
-
-  it('should create a revision for existing item if revisions frequency is matched', async () => {
-    timer.convertMicrosecondsToSeconds = itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
-
-    const result = await createService().saveItems({
-      itemHashes: [itemHash1],
-      userUuid: '1-2-3',
-      apiVersion: ApiVersion.v20200115,
-      readOnlyAccess: false,
-      sessionUuid: '2-3-4',
-    })
-
-    expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
-        {
-          content: 'asdqwe1',
-          contentSize: 950,
-          contentType: 'Note',
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          encItemKey: 'qweqwe1',
-          itemsKeyId: 'asdasd1',
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
-        },
-      ],
-      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
-    })
-  })
-
-  it('should update existing items with empty user-agent', async () => {
-    itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
-
-    const result = await createService().saveItems({
-      itemHashes: [itemHash1],
-      userUuid: '1-2-3',
-      apiVersion: ApiVersion.v20200115,
-      readOnlyAccess: false,
-      sessionUuid: '2-3-4',
-    })
-
-    expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
-        {
-          content: 'asdqwe1',
-          contentSize: 950,
-          contentType: 'Note',
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          encItemKey: 'qweqwe1',
-          itemsKeyId: 'asdasd1',
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
-        },
-      ],
-      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
-    })
-  })
-
-  it('should update existing items with auth hash', async () => {
-    itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
-
-    itemHash1.auth_hash = 'test'
-
-    const result = await createService().saveItems({
-      itemHashes: [itemHash1],
-      userUuid: '1-2-3',
-      apiVersion: ApiVersion.v20200115,
-      readOnlyAccess: false,
-      sessionUuid: '2-3-4',
-    })
-
-    expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
+      conflicts: [
         {
         {
-          content: 'asdqwe1',
-          contentSize: 950,
-          contentType: 'Note',
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          encItemKey: 'qweqwe1',
-          itemsKeyId: 'asdasd1',
-          authHash: 'test',
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
+          type: 'uuid_conflict',
+          unsavedItem: itemHash1,
         },
         },
       ],
       ],
+      savedItems: [],
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
     })
     })
   })
   })
 
 
-  it('should mark existing item as deleted', async () => {
-    itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
+  it('should skip saving conflicting items and mark them as sync conflicts when saving fails', async () => {
+    itemRepository.findByUuid = jest.fn().mockReturnValue(null)
+    saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
 
 
-    itemHash1.deleted = true
     const result = await createService().saveItems({
     const result = await createService().saveItems({
-      itemHashes: [itemHash1],
+      itemHashes: [itemHash1, itemHash2],
       userUuid: '1-2-3',
       userUuid: '1-2-3',
       apiVersion: ApiVersion.v20200115,
       apiVersion: ApiVersion.v20200115,
       readOnlyAccess: false,
       readOnlyAccess: false,
@@ -877,71 +738,25 @@ describe('ItemService', () => {
     })
     })
 
 
     expect(result).toEqual({
     expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
+      conflicts: [
         {
         {
-          content: null,
-          contentSize: 0,
-          authHash: null,
-          contentType: 'Note',
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          encItemKey: null,
-          deleted: true,
-          itemsKeyId: null,
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
+          type: 'uuid_conflict',
+          unsavedItem: itemHash1,
         },
         },
-      ],
-      syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
-    })
-  })
-
-  it('should mark existing item as duplicate', async () => {
-    itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
-
-    itemHash1.duplicate_of = '1-2-3'
-    const result = await createService().saveItems({
-      itemHashes: [itemHash1],
-      userUuid: '1-2-3',
-      apiVersion: ApiVersion.v20200115,
-      readOnlyAccess: false,
-      sessionUuid: '2-3-4',
-    })
-
-    expect(result).toEqual({
-      conflicts: [],
-      savedItems: [
         {
         {
-          content: 'asdqwe1',
-          contentSize: 950,
-          contentType: 'Note',
-          createdAtTimestamp: expect.any(Number),
-          createdAt: expect.any(Date),
-          encItemKey: 'qweqwe1',
-          duplicateOf: '1-2-3',
-          itemsKeyId: 'asdasd1',
-          userUuid: '1-2-3',
-          updatedAtTimestamp: expect.any(Number),
-          updatedAt: expect.any(Date),
-          updatedWithSession: '2-3-4',
-          uuid: '1-2-3',
+          type: 'uuid_conflict',
+          unsavedItem: itemHash2,
         },
         },
       ],
       ],
+      savedItems: [],
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
       syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
     })
     })
-    expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
-    expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalledTimes(1)
-    expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalledTimes(1)
   })
   })
 
 
-  it('should skip saving conflicting items and mark them as sync conflicts when saving to database fails', async () => {
+  it('should skip saving conflicting items and mark them as sync conflicts when saving throws an error', async () => {
     itemRepository.findByUuid = jest.fn().mockReturnValue(null)
     itemRepository.findByUuid = jest.fn().mockReturnValue(null)
-    itemRepository.save = jest.fn().mockImplementation(() => {
-      throw new Error('Something bad happened')
+    saveNewItemUseCase.execute = jest.fn().mockImplementation(() => {
+      throw new Error('Oops')
     })
     })
 
 
     const result = await createService().saveItems({
     const result = await createService().saveItems({

+ 45 - 117
packages/syncing-server/src/Domain/Item/ItemService.ts

@@ -1,15 +1,10 @@
-import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import { Time, TimerInterface } from '@standardnotes/time'
 import { Time, TimerInterface } from '@standardnotes/time'
-import { ContentType } from '@standardnotes/common'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
 
 
-import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { GetItemsDTO } from './GetItemsDTO'
 import { GetItemsDTO } from './GetItemsDTO'
 import { GetItemsResult } from './GetItemsResult'
 import { GetItemsResult } from './GetItemsResult'
 import { Item } from './Item'
 import { Item } from './Item'
 import { ItemConflict } from './ItemConflict'
 import { ItemConflict } from './ItemConflict'
-import { ItemFactoryInterface } from './ItemFactoryInterface'
-import { ItemHash } from './ItemHash'
 import { ItemQuery } from './ItemQuery'
 import { ItemQuery } from './ItemQuery'
 import { ItemRepositoryInterface } from './ItemRepositoryInterface'
 import { ItemRepositoryInterface } from './ItemRepositoryInterface'
 import { ItemServiceInterface } from './ItemServiceInterface'
 import { ItemServiceInterface } from './ItemServiceInterface'
@@ -18,8 +13,9 @@ import { SaveItemsResult } from './SaveItemsResult'
 import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
 import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
 import { ConflictType } from '@standardnotes/responses'
 import { ConflictType } from '@standardnotes/responses'
 import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
 import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { ItemProjection } from '../../Projection/ItemProjection'
+import { SaveNewItem } from '../UseCase/Syncing/SaveNewItem/SaveNewItem'
+import { ContentType } from '@standardnotes/domain-core'
+import { UpdateExistingItem } from '../UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
 
 
 export class ItemService implements ItemServiceInterface {
 export class ItemService implements ItemServiceInterface {
   private readonly DEFAULT_ITEMS_LIMIT = 150
   private readonly DEFAULT_ITEMS_LIMIT = 150
@@ -27,16 +23,13 @@ export class ItemService implements ItemServiceInterface {
 
 
   constructor(
   constructor(
     private itemSaveValidator: ItemSaveValidatorInterface,
     private itemSaveValidator: ItemSaveValidatorInterface,
-    private itemFactory: ItemFactoryInterface,
     private itemRepository: ItemRepositoryInterface,
     private itemRepository: ItemRepositoryInterface,
-    private domainEventPublisher: DomainEventPublisherInterface,
-    private domainEventFactory: DomainEventFactoryInterface,
-    private revisionFrequency: number,
     private contentSizeTransferLimit: number,
     private contentSizeTransferLimit: number,
     private itemTransferCalculator: ItemTransferCalculatorInterface,
     private itemTransferCalculator: ItemTransferCalculatorInterface,
     private timer: TimerInterface,
     private timer: TimerInterface,
-    private itemProjector: ProjectorInterface<Item, ItemProjection>,
     private maxItemsSyncLimit: number,
     private maxItemsSyncLimit: number,
+    private saveNewItem: SaveNewItem,
+    private updateExistingItem: UpdateExistingItem,
     private logger: Logger,
     private logger: Logger,
   ) {}
   ) {}
 
 
@@ -73,7 +66,7 @@ export class ItemService implements ItemServiceInterface {
 
 
     let cursorToken = undefined
     let cursorToken = undefined
     if (totalItemsCount > upperBoundLimit) {
     if (totalItemsCount > upperBoundLimit) {
-      const lastSyncTime = items[items.length - 1].updatedAtTimestamp / Time.MicrosecondsInASecond
+      const lastSyncTime = items[items.length - 1].props.timestamps.updatedAt / Time.MicrosecondsInASecond
       cursorToken = Buffer.from(`${this.SYNC_TOKEN_VERSION}:${lastSyncTime}`, 'utf-8').toString('base64')
       cursorToken = Buffer.from(`${this.SYNC_TOKEN_VERSION}:${lastSyncTime}`, 'utf-8').toString('base64')
     }
     }
 
 
@@ -118,15 +111,47 @@ export class ItemService implements ItemServiceInterface {
       }
       }
 
 
       if (existingItem) {
       if (existingItem) {
-        const updatedItem = await this.updateExistingItem({
+        const udpatedItemOrError = await this.updateExistingItem.execute({
           existingItem,
           existingItem,
           itemHash,
           itemHash,
           sessionUuid: dto.sessionUuid,
           sessionUuid: dto.sessionUuid,
         })
         })
+        if (udpatedItemOrError.isFailed()) {
+          this.logger.error(
+            `[${dto.userUuid}] Updating item ${itemHash.uuid} failed. Error: ${udpatedItemOrError.getError()}`,
+          )
+
+          conflicts.push({
+            unsavedItem: itemHash,
+            type: ConflictType.UuidConflict,
+          })
+
+          continue
+        }
+        const updatedItem = udpatedItemOrError.getValue()
+
         savedItems.push(updatedItem)
         savedItems.push(updatedItem)
       } else {
       } else {
         try {
         try {
-          const newItem = await this.saveNewItem({ userUuid: dto.userUuid, itemHash, sessionUuid: dto.sessionUuid })
+          const newItemOrError = await this.saveNewItem.execute({
+            userUuid: dto.userUuid,
+            itemHash,
+            sessionUuid: dto.sessionUuid,
+          })
+          if (newItemOrError.isFailed()) {
+            this.logger.error(
+              `[${dto.userUuid}] Saving item ${itemHash.uuid} failed. Error: ${newItemOrError.getError()}`,
+            )
+
+            conflicts.push({
+              unsavedItem: itemHash,
+              type: ConflictType.UuidConflict,
+            })
+
+            continue
+          }
+          const newItem = newItemOrError.getValue()
+
           savedItems.push(newItem)
           savedItems.push(newItem)
         } catch (error) {
         } catch (error) {
           this.logger.error(`[${dto.userUuid}] Saving item ${itemHash.uuid} failed. Error: ${(error as Error).message}`)
           this.logger.error(`[${dto.userUuid}] Saving item ${itemHash.uuid} failed. Error: ${(error as Error).message}`)
@@ -153,15 +178,15 @@ export class ItemService implements ItemServiceInterface {
   async frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>> {
   async frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>> {
     const itemsKeys = await this.itemRepository.findAll({
     const itemsKeys = await this.itemRepository.findAll({
       userUuid,
       userUuid,
-      contentType: ContentType.ItemsKey,
+      contentType: ContentType.TYPES.ItemsKey,
       sortBy: 'updated_at_timestamp',
       sortBy: 'updated_at_timestamp',
       sortOrder: 'ASC',
       sortOrder: 'ASC',
     })
     })
 
 
-    const retrievedItemsIds: Array<string> = retrievedItems.map((item: Item) => item.uuid)
+    const retrievedItemsIds: Array<string> = retrievedItems.map((item: Item) => item.id.toString())
 
 
     itemsKeys.forEach((itemKey: Item) => {
     itemsKeys.forEach((itemKey: Item) => {
-      if (retrievedItemsIds.indexOf(itemKey.uuid) === -1) {
+      if (retrievedItemsIds.indexOf(itemKey.id.toString()) === -1) {
         retrievedItems.unshift(itemKey)
         retrievedItems.unshift(itemKey)
       }
       }
     })
     })
@@ -172,9 +197,9 @@ export class ItemService implements ItemServiceInterface {
   private calculateSyncToken(lastUpdatedTimestamp: number, savedItems: Array<Item>): string {
   private calculateSyncToken(lastUpdatedTimestamp: number, savedItems: Array<Item>): string {
     if (savedItems.length) {
     if (savedItems.length) {
       const sortedItems = savedItems.sort((itemA: Item, itemB: Item) => {
       const sortedItems = savedItems.sort((itemA: Item, itemB: Item) => {
-        return itemA.updatedAtTimestamp > itemB.updatedAtTimestamp ? 1 : -1
+        return itemA.props.timestamps.updatedAt > itemB.props.timestamps.updatedAt ? 1 : -1
       })
       })
-      lastUpdatedTimestamp = sortedItems[sortedItems.length - 1].updatedAtTimestamp
+      lastUpdatedTimestamp = sortedItems[sortedItems.length - 1].props.timestamps.updatedAt
     }
     }
 
 
     const lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles = lastUpdatedTimestamp + 1
     const lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles = lastUpdatedTimestamp + 1
@@ -187,103 +212,6 @@ export class ItemService implements ItemServiceInterface {
     ).toString('base64')
     ).toString('base64')
   }
   }
 
 
-  private async updateExistingItem(dto: {
-    existingItem: Item
-    itemHash: ItemHash
-    sessionUuid: string | null
-  }): Promise<Item> {
-    dto.existingItem.updatedWithSession = dto.sessionUuid
-    dto.existingItem.contentSize = 0
-    if (dto.itemHash.content) {
-      dto.existingItem.content = dto.itemHash.content
-    }
-    if (dto.itemHash.content_type) {
-      dto.existingItem.contentType = dto.itemHash.content_type
-    }
-    if (dto.itemHash.deleted !== undefined) {
-      dto.existingItem.deleted = dto.itemHash.deleted
-    }
-    let wasMarkedAsDuplicate = false
-    if (dto.itemHash.duplicate_of) {
-      wasMarkedAsDuplicate = !dto.existingItem.duplicateOf
-      dto.existingItem.duplicateOf = dto.itemHash.duplicate_of
-    }
-    if (dto.itemHash.auth_hash) {
-      dto.existingItem.authHash = dto.itemHash.auth_hash
-    }
-    if (dto.itemHash.enc_item_key) {
-      dto.existingItem.encItemKey = dto.itemHash.enc_item_key
-    }
-    if (dto.itemHash.items_key_id) {
-      dto.existingItem.itemsKeyId = dto.itemHash.items_key_id
-    }
-
-    const updatedAt = this.timer.getTimestampInMicroseconds()
-    const secondsFromLastUpdate = this.timer.convertMicrosecondsToSeconds(
-      updatedAt - dto.existingItem.updatedAtTimestamp,
-    )
-
-    if (dto.itemHash.created_at_timestamp) {
-      dto.existingItem.createdAtTimestamp = dto.itemHash.created_at_timestamp
-      dto.existingItem.createdAt = this.timer.convertMicrosecondsToDate(dto.itemHash.created_at_timestamp)
-    } else if (dto.itemHash.created_at) {
-      dto.existingItem.createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
-      dto.existingItem.createdAt = this.timer.convertStringDateToDate(dto.itemHash.created_at)
-    }
-
-    dto.existingItem.updatedAtTimestamp = updatedAt
-    dto.existingItem.updatedAt = this.timer.convertMicrosecondsToDate(updatedAt)
-
-    dto.existingItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(dto.existingItem)))
-
-    if (dto.itemHash.deleted === true) {
-      dto.existingItem.deleted = true
-      dto.existingItem.content = null
-      dto.existingItem.contentSize = 0
-      dto.existingItem.encItemKey = null
-      dto.existingItem.authHash = null
-      dto.existingItem.itemsKeyId = null
-    }
-
-    const savedItem = await this.itemRepository.save(dto.existingItem)
-
-    if (secondsFromLastUpdate >= this.revisionFrequency) {
-      if ([ContentType.Note, ContentType.File].includes(savedItem.contentType as ContentType)) {
-        await this.domainEventPublisher.publish(
-          this.domainEventFactory.createItemRevisionCreationRequested(savedItem.uuid, savedItem.userUuid),
-        )
-      }
-    }
-
-    if (wasMarkedAsDuplicate) {
-      await this.domainEventPublisher.publish(
-        this.domainEventFactory.createDuplicateItemSyncedEvent(savedItem.uuid, savedItem.userUuid),
-      )
-    }
-
-    return savedItem
-  }
-
-  private async saveNewItem(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Promise<Item> {
-    const newItem = this.itemFactory.create(dto)
-
-    const savedItem = await this.itemRepository.save(newItem)
-
-    if ([ContentType.Note, ContentType.File].includes(savedItem.contentType as ContentType)) {
-      await this.domainEventPublisher.publish(
-        this.domainEventFactory.createItemRevisionCreationRequested(savedItem.uuid, savedItem.userUuid),
-      )
-    }
-
-    if (savedItem.duplicateOf) {
-      await this.domainEventPublisher.publish(
-        this.domainEventFactory.createDuplicateItemSyncedEvent(savedItem.uuid, savedItem.userUuid),
-      )
-    }
-
-    return savedItem
-  }
-
   private getLastSyncTime(dto: GetItemsDTO): number | undefined {
   private getLastSyncTime(dto: GetItemsDTO): number | undefined {
     let token = dto.syncToken
     let token = dto.syncToken
     if (dto.cursorToken !== undefined && dto.cursorToken !== null) {
     if (dto.cursorToken !== undefined && dto.cursorToken !== null) {

+ 4 - 5
packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.spec.ts

@@ -1,11 +1,10 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
 
 
-import { ContentType } from '@standardnotes/common'
-
 import { ApiVersion } from '../../Api/ApiVersion'
 import { ApiVersion } from '../../Api/ApiVersion'
 import { Item } from '../Item'
 import { Item } from '../Item'
 
 
 import { ContentFilter } from './ContentFilter'
 import { ContentFilter } from './ContentFilter'
+import { ContentType } from '@standardnotes/domain-core'
 
 
 describe('ContentFilter', () => {
 describe('ContentFilter', () => {
   let existingItem: Item
   let existingItem: Item
@@ -21,7 +20,7 @@ describe('ContentFilter', () => {
         itemHash: {
         itemHash: {
           uuid: '123e4567-e89b-12d3-a456-426655440000',
           uuid: '123e4567-e89b-12d3-a456-426655440000',
           content: invalidContent as unknown as string,
           content: invalidContent as unknown as string,
-          content_type: ContentType.Note,
+          content_type: ContentType.TYPES.Note,
         },
         },
         existingItem: null,
         existingItem: null,
       })
       })
@@ -32,7 +31,7 @@ describe('ContentFilter', () => {
           unsavedItem: {
           unsavedItem: {
             uuid: '123e4567-e89b-12d3-a456-426655440000',
             uuid: '123e4567-e89b-12d3-a456-426655440000',
             content: invalidContent,
             content: invalidContent,
-            content_type: ContentType.Note,
+            content_type: ContentType.TYPES.Note,
           },
           },
           type: 'content_error',
           type: 'content_error',
         },
         },
@@ -50,7 +49,7 @@ describe('ContentFilter', () => {
         itemHash: {
         itemHash: {
           uuid: '123e4567-e89b-12d3-a456-426655440000',
           uuid: '123e4567-e89b-12d3-a456-426655440000',
           content: validContent as unknown as string,
           content: validContent as unknown as string,
-          content_type: ContentType.Note,
+          content_type: ContentType.TYPES.Note,
         },
         },
         existingItem,
         existingItem,
       })
       })

+ 3 - 3
packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.spec.ts

@@ -1,5 +1,5 @@
-import { ContentType } from '@standardnotes/common'
 import 'reflect-metadata'
 import 'reflect-metadata'
+
 import { ApiVersion } from '../../Api/ApiVersion'
 import { ApiVersion } from '../../Api/ApiVersion'
 import { Item } from '../Item'
 import { Item } from '../Item'
 
 
@@ -27,7 +27,7 @@ describe('ContentTypeFilter', () => {
         apiVersion: ApiVersion.v20200115,
         apiVersion: ApiVersion.v20200115,
         itemHash: {
         itemHash: {
           uuid: '123e4567-e89b-12d3-a456-426655440000',
           uuid: '123e4567-e89b-12d3-a456-426655440000',
-          content_type: invalidContentType as ContentType,
+          content_type: invalidContentType,
         },
         },
         existingItem: null,
         existingItem: null,
       })
       })
@@ -54,7 +54,7 @@ describe('ContentTypeFilter', () => {
         apiVersion: ApiVersion.v20200115,
         apiVersion: ApiVersion.v20200115,
         itemHash: {
         itemHash: {
           uuid: '123e4567-e89b-12d3-a456-426655440000',
           uuid: '123e4567-e89b-12d3-a456-426655440000',
-          content_type: validContentType as ContentType,
+          content_type: validContentType,
         },
         },
         existingItem,
         existingItem,
       })
       })

+ 4 - 5
packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts

@@ -1,15 +1,14 @@
-import { ContentType } from '@standardnotes/common'
+import { ConflictType } from '@standardnotes/responses'
+import { ContentType } from '@standardnotes/domain-core'
 
 
 import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
 import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
 import { ItemSaveRuleResult } from './ItemSaveRuleResult'
 import { ItemSaveRuleResult } from './ItemSaveRuleResult'
 import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
 import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
-import { ConflictType } from '@standardnotes/responses'
 
 
 export class ContentTypeFilter implements ItemSaveRuleInterface {
 export class ContentTypeFilter implements ItemSaveRuleInterface {
   async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
   async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
-    const validContentType = Object.values(ContentType).includes(dto.itemHash.content_type as ContentType)
-
-    if (!validContentType) {
+    const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
+    if (contentTypeOrError.isFailed()) {
       return {
       return {
         passed: false,
         passed: false,
         conflict: {
         conflict: {

+ 47 - 13
packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.spec.ts

@@ -1,28 +1,41 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
 
 
-import { ContentType } from '@standardnotes/common'
-
 import { ApiVersion } from '../../Api/ApiVersion'
 import { ApiVersion } from '../../Api/ApiVersion'
 import { Item } from '../Item'
 import { Item } from '../Item'
 
 
 import { OwnershipFilter } from './OwnershipFilter'
 import { OwnershipFilter } from './OwnershipFilter'
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
 
 
 describe('OwnershipFilter', () => {
 describe('OwnershipFilter', () => {
   let existingItem: Item
   let existingItem: Item
   const createFilter = () => new OwnershipFilter()
   const createFilter = () => new OwnershipFilter()
 
 
   beforeEach(() => {
   beforeEach(() => {
-    existingItem = {} as jest.Mocked<Item>
-    existingItem.userUuid = '2-3-4'
+    existingItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
   })
   })
 
 
   it('should filter out items belonging to a different user', async () => {
   it('should filter out items belonging to a different user', async () => {
     const result = await createFilter().check({
     const result = await createFilter().check({
-      userUuid: '1-2-3',
+      userUuid: '00000000-0000-0000-0000-000000000001',
       apiVersion: ApiVersion.v20200115,
       apiVersion: ApiVersion.v20200115,
       itemHash: {
       itemHash: {
         uuid: '2-3-4',
         uuid: '2-3-4',
-        content_type: ContentType.Note,
+        content_type: ContentType.TYPES.Note,
       },
       },
       existingItem,
       existingItem,
     })
     })
@@ -32,7 +45,7 @@ describe('OwnershipFilter', () => {
       conflict: {
       conflict: {
         unsavedItem: {
         unsavedItem: {
           uuid: '2-3-4',
           uuid: '2-3-4',
-          content_type: ContentType.Note,
+          content_type: ContentType.TYPES.Note,
         },
         },
         type: 'uuid_conflict',
         type: 'uuid_conflict',
       },
       },
@@ -40,14 +53,12 @@ describe('OwnershipFilter', () => {
   })
   })
 
 
   it('should leave items belonging to the same user', async () => {
   it('should leave items belonging to the same user', async () => {
-    existingItem.userUuid = '1-2-3'
-
     const result = await createFilter().check({
     const result = await createFilter().check({
-      userUuid: '1-2-3',
+      userUuid: '00000000-0000-0000-0000-000000000000',
       apiVersion: ApiVersion.v20200115,
       apiVersion: ApiVersion.v20200115,
       itemHash: {
       itemHash: {
         uuid: '2-3-4',
         uuid: '2-3-4',
-        content_type: ContentType.Note,
+        content_type: ContentType.TYPES.Note,
       },
       },
       existingItem,
       existingItem,
     })
     })
@@ -59,11 +70,11 @@ describe('OwnershipFilter', () => {
 
 
   it('should leave non existing items', async () => {
   it('should leave non existing items', async () => {
     const result = await createFilter().check({
     const result = await createFilter().check({
-      userUuid: '1-2-3',
+      userUuid: '00000000-0000-0000-0000-000000000000',
       apiVersion: ApiVersion.v20200115,
       apiVersion: ApiVersion.v20200115,
       itemHash: {
       itemHash: {
         uuid: '2-3-4',
         uuid: '2-3-4',
-        content_type: ContentType.Note,
+        content_type: ContentType.TYPES.Note,
       },
       },
       existingItem: null,
       existingItem: null,
     })
     })
@@ -72,4 +83,27 @@ describe('OwnershipFilter', () => {
       passed: true,
       passed: true,
     })
     })
   })
   })
+
+  it('should return an error if the user uuid is invalid', async () => {
+    const result = await createFilter().check({
+      userUuid: 'invalid',
+      apiVersion: ApiVersion.v20200115,
+      itemHash: {
+        uuid: '2-3-4',
+        content_type: ContentType.TYPES.Note,
+      },
+      existingItem,
+    })
+
+    expect(result).toEqual({
+      passed: false,
+      conflict: {
+        unsavedItem: {
+          uuid: '2-3-4',
+          content_type: ContentType.TYPES.Note,
+        },
+        type: 'uuid_error',
+      },
+    })
+  })
 })
 })

+ 14 - 1
packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts

@@ -2,10 +2,23 @@ import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
 import { ItemSaveRuleResult } from './ItemSaveRuleResult'
 import { ItemSaveRuleResult } from './ItemSaveRuleResult'
 import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
 import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
 import { ConflictType } from '@standardnotes/responses'
 import { ConflictType } from '@standardnotes/responses'
+import { Uuid } from '@standardnotes/domain-core'
 
 
 export class OwnershipFilter implements ItemSaveRuleInterface {
 export class OwnershipFilter implements ItemSaveRuleInterface {
   async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
   async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
-    const itemBelongsToADifferentUser = dto.existingItem !== null && dto.existingItem.userUuid !== dto.userUuid
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return {
+        passed: false,
+        conflict: {
+          unsavedItem: dto.itemHash,
+          type: ConflictType.UuidError,
+        },
+      }
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const itemBelongsToADifferentUser = dto.existingItem !== null && !dto.existingItem.props.userUuid.equals(userUuid)
     if (itemBelongsToADifferentUser) {
     if (itemBelongsToADifferentUser) {
       return {
       return {
         passed: false,
         passed: false,

+ 28 - 19
packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts

@@ -1,8 +1,7 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
 
 
-import { ContentType } from '@standardnotes/common'
-
 import { Time, Timer, TimerInterface } from '@standardnotes/time'
 import { Time, Timer, TimerInterface } from '@standardnotes/time'
+import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
 
 
 import { ApiVersion } from '../../Api/ApiVersion'
 import { ApiVersion } from '../../Api/ApiVersion'
 
 
@@ -26,28 +25,36 @@ describe('TimeDifferenceFilter', () => {
       .fn()
       .fn()
       .mockImplementation((date: string) => timeHelper.convertStringDateToMicroseconds(date))
       .mockImplementation((date: string) => timeHelper.convertStringDateToMicroseconds(date))
 
 
-    existingItem = {
-      uuid: '1-2-3',
-      userUuid: '1-2-3',
-      createdAt: new Date(1616164633241311),
-      createdAtTimestamp: 1616164633241311,
-      updatedAt: new Date(1616164633241311),
-      updatedAtTimestamp: 1616164633241311,
-    } as jest.Mocked<Item>
+    existingItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
 
 
     itemHash = {
     itemHash = {
       uuid: '1-2-3',
       uuid: '1-2-3',
       content: 'asdqwe1',
       content: 'asdqwe1',
-      content_type: ContentType.Note,
+      content_type: ContentType.TYPES.Note,
       duplicate_of: null,
       duplicate_of: null,
       enc_item_key: 'qweqwe1',
       enc_item_key: 'qweqwe1',
       items_key_id: 'asdasd1',
       items_key_id: 'asdasd1',
       created_at: timeHelper.formatDate(
       created_at: timeHelper.formatDate(
-        timeHelper.convertMicrosecondsToDate(existingItem.createdAtTimestamp),
+        timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.createdAt),
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       ),
       ),
       updated_at: timeHelper.formatDate(
       updated_at: timeHelper.formatDate(
-        timeHelper.convertMicrosecondsToDate(existingItem.updatedAtTimestamp + 1),
+        timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt + 1),
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
         'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       ),
       ),
     } as jest.Mocked<ItemHash>
     } as jest.Mocked<ItemHash>
@@ -83,7 +90,7 @@ describe('TimeDifferenceFilter', () => {
   })
   })
 
 
   it('should filter out items having update at timestamp different in microseconds precision', async () => {
   it('should filter out items having update at timestamp different in microseconds precision', async () => {
-    itemHash.updated_at_timestamp = existingItem.updatedAtTimestamp + 1
+    itemHash.updated_at_timestamp = existingItem.props.timestamps.updatedAt + 1
 
 
     const result = await createFilter().check({
     const result = await createFilter().check({
       userUuid: '1-2-3',
       userUuid: '1-2-3',
@@ -102,7 +109,7 @@ describe('TimeDifferenceFilter', () => {
   })
   })
 
 
   it('should leave items having update at timestamp same in microseconds precision', async () => {
   it('should leave items having update at timestamp same in microseconds precision', async () => {
-    itemHash.updated_at_timestamp = existingItem.updatedAtTimestamp
+    itemHash.updated_at_timestamp = existingItem.props.timestamps.updatedAt
 
 
     const result = await createFilter().check({
     const result = await createFilter().check({
       userUuid: '1-2-3',
       userUuid: '1-2-3',
@@ -119,7 +126,9 @@ describe('TimeDifferenceFilter', () => {
   it('should filter out items having update at timestamp different by a second for legacy clients', async () => {
   it('should filter out items having update at timestamp different by a second for legacy clients', async () => {
     itemHash.updated_at = timeHelper.formatDate(
     itemHash.updated_at = timeHelper.formatDate(
       new Date(
       new Date(
-        timeHelper.convertMicrosecondsToMilliseconds(existingItem.updatedAtTimestamp) + Time.MicrosecondsInASecond + 1,
+        timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
+          Time.MicrosecondsInASecond +
+          1,
       ),
       ),
       'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
     )
     )
@@ -142,7 +151,7 @@ describe('TimeDifferenceFilter', () => {
 
 
   it('should leave items having update at timestamp different by less then a second for legacy clients', async () => {
   it('should leave items having update at timestamp different by less then a second for legacy clients', async () => {
     itemHash.updated_at = timeHelper.formatDate(
     itemHash.updated_at = timeHelper.formatDate(
-      timeHelper.convertMicrosecondsToDate(existingItem.updatedAtTimestamp),
+      timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
       'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
     )
     )
 
 
@@ -161,7 +170,7 @@ describe('TimeDifferenceFilter', () => {
   it('should filter out items having update at timestamp different by a millisecond', async () => {
   it('should filter out items having update at timestamp different by a millisecond', async () => {
     itemHash.updated_at = timeHelper.formatDate(
     itemHash.updated_at = timeHelper.formatDate(
       new Date(
       new Date(
-        timeHelper.convertMicrosecondsToMilliseconds(existingItem.updatedAtTimestamp) +
+        timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
           Time.MicrosecondsInAMillisecond +
           Time.MicrosecondsInAMillisecond +
           1,
           1,
       ),
       ),
@@ -186,7 +195,7 @@ describe('TimeDifferenceFilter', () => {
 
 
   it('should leave items having update at timestamp different by less than a millisecond', async () => {
   it('should leave items having update at timestamp different by less than a millisecond', async () => {
     itemHash.updated_at = timeHelper.formatDate(
     itemHash.updated_at = timeHelper.formatDate(
-      timeHelper.convertMicrosecondsToDate(existingItem.updatedAtTimestamp),
+      timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
       'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
       'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
     )
     )
 
 

+ 1 - 1
packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts

@@ -31,7 +31,7 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface {
       }
       }
     }
     }
 
 
-    const ourUpdatedAtTimestamp = dto.existingItem.updatedAtTimestamp
+    const ourUpdatedAtTimestamp = dto.existingItem.props.timestamps.updatedAt
     const difference = incomingUpdatedAtTimestamp - ourUpdatedAtTimestamp
     const difference = incomingUpdatedAtTimestamp - ourUpdatedAtTimestamp
 
 
     if (this.itemHashHasMicrosecondsPrecision(dto.itemHash)) {
     if (this.itemHashHasMicrosecondsPrecision(dto.itemHash)) {

+ 0 - 72
packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.spec.ts

@@ -1,72 +0,0 @@
-import 'reflect-metadata'
-
-import { ContentType } from '@standardnotes/common'
-
-import { ApiVersion } from '../../Api/ApiVersion'
-import { Item } from '../Item'
-
-import { UuidFilter } from './UuidFilter'
-
-describe('UuidFilter', () => {
-  const createFilter = () => new UuidFilter()
-
-  it('should filter out items with invalid uuid', async () => {
-    const invalidUuids = [
-      'c73bcdcc-2669-4bf6-81d3-e4an73fb11fd',
-      'c73bcdcc26694bf681d3e4ae73fb11fd',
-      'definitely-not-a-uuid',
-      '1-2-3',
-      'test',
-      "(select load_file('\\\\\\\\iugt7mazsk477",
-      '/etc/passwd',
-      "eval(compile('for x in range(1):\\n i",
-    ]
-
-    for (const invalidUuid of invalidUuids) {
-      const result = await createFilter().check({
-        userUuid: '1-2-3',
-        apiVersion: ApiVersion.v20200115,
-        itemHash: {
-          uuid: invalidUuid,
-          content_type: ContentType.Note,
-        },
-        existingItem: null,
-      })
-
-      expect(result).toEqual({
-        passed: false,
-        conflict: {
-          unsavedItem: {
-            uuid: invalidUuid,
-            content_type: ContentType.Note,
-          },
-          type: 'uuid_error',
-        },
-      })
-    }
-  })
-
-  it('should leave items with valid uuid', async () => {
-    const validUuids = [
-      '123e4567-e89b-12d3-a456-426655440000',
-      'c73bcdcc-2669-4bf6-81d3-e4ae73fb11fd',
-      'C73BCDCC-2669-4Bf6-81d3-E4AE73FB11FD',
-    ]
-
-    for (const validUuid of validUuids) {
-      const result = await createFilter().check({
-        userUuid: '1-2-3',
-        apiVersion: ApiVersion.v20200115,
-        itemHash: {
-          uuid: validUuid,
-          content_type: ContentType.Note,
-        },
-        existingItem: {} as jest.Mocked<Item>,
-      })
-
-      expect(result).toEqual({
-        passed: true,
-      })
-    }
-  })
-})

+ 0 - 25
packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts

@@ -1,25 +0,0 @@
-import { validate } from 'uuid'
-import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
-import { ItemSaveRuleResult } from './ItemSaveRuleResult'
-import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
-import { ConflictType } from '@standardnotes/responses'
-
-export class UuidFilter implements ItemSaveRuleInterface {
-  async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
-    const validUuid = validate(dto.itemHash.uuid)
-
-    if (!validUuid) {
-      return {
-        passed: false,
-        conflict: {
-          unsavedItem: dto.itemHash,
-          type: ConflictType.UuidError,
-        },
-      }
-    }
-
-    return {
-      passed: true,
-    }
-  }
-}

+ 4 - 4
packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts

@@ -1,13 +1,13 @@
 import { ConflictType } from '@standardnotes/responses'
 import { ConflictType } from '@standardnotes/responses'
 
 
 import { ItemHash } from '../ItemHash'
 import { ItemHash } from '../ItemHash'
-import { ItemProjection } from '../../../Projection/ItemProjection'
+import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
 
 
 export type SyncResponse20161215 = {
 export type SyncResponse20161215 = {
-  retrieved_items: Array<ItemProjection>
-  saved_items: Array<ItemProjection>
+  retrieved_items: Array<ItemHttpRepresentation>
+  saved_items: Array<ItemHttpRepresentation>
   unsaved: Array<{
   unsaved: Array<{
-    item: ItemProjection | ItemHash
+    item: ItemHttpRepresentation | ItemHash
     error: {
     error: {
       tag: ConflictType
       tag: ConflictType
     }
     }

+ 6 - 6
packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts

@@ -1,11 +1,11 @@
-import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection'
-import { ItemProjection } from '../../../Projection/ItemProjection'
-import { SavedItemProjection } from '../../../Projection/SavedItemProjection'
+import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation'
+import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
+import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation'
 
 
 export type SyncResponse20200115 = {
 export type SyncResponse20200115 = {
-  retrieved_items: Array<ItemProjection>
-  saved_items: Array<SavedItemProjection>
-  conflicts: Array<ItemConflictProjection>
+  retrieved_items: Array<ItemHttpRepresentation>
+  saved_items: Array<SavedItemHttpRepresentation>
+  conflicts: Array<ItemConflictHttpRepresentation>
   sync_token: string
   sync_token: string
   cursor_token?: string
   cursor_token?: string
 }
 }

+ 54 - 21
packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts

@@ -1,16 +1,16 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
-import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
 
 
 import { Item } from '../Item'
 import { Item } from '../Item'
 import { ItemHash } from '../ItemHash'
 import { ItemHash } from '../ItemHash'
-import { ItemProjection } from '../../../Projection/ItemProjection'
 import { SyncResponseFactory20161215 } from './SyncResponseFactory20161215'
 import { SyncResponseFactory20161215 } from './SyncResponseFactory20161215'
 import { ConflictType } from '@standardnotes/responses'
 import { ConflictType } from '@standardnotes/responses'
+import { ContentType, Dates, MapperInterface, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
+import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
 
 
 describe('SyncResponseFactory20161215', () => {
 describe('SyncResponseFactory20161215', () => {
-  let itemProjector: ProjectorInterface<Item, ItemProjection>
-  let item1Projection: ItemProjection
-  let item2Projection: ItemProjection
+  let itemProjector: MapperInterface<Item, ItemHttpRepresentation>
+  let item1Projection: ItemHttpRepresentation
+  let item2Projection: ItemHttpRepresentation
   let item1: Item
   let item1: Item
   let item2: Item
   let item2: Item
 
 
@@ -19,30 +19,55 @@ describe('SyncResponseFactory20161215', () => {
   beforeEach(() => {
   beforeEach(() => {
     item1Projection = {
     item1Projection = {
       uuid: '1-2-3',
       uuid: '1-2-3',
-    } as jest.Mocked<ItemProjection>
+    } as jest.Mocked<ItemHttpRepresentation>
     item2Projection = {
     item2Projection = {
       uuid: '2-3-4',
       uuid: '2-3-4',
-    } as jest.Mocked<ItemProjection>
+    } as jest.Mocked<ItemHttpRepresentation>
 
 
-    itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
-    itemProjector.projectFull = jest.fn().mockImplementation((item: Item) => {
-      if (item.uuid === '1-2-3') {
+    itemProjector = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
+    itemProjector.toProjection = jest.fn().mockImplementation((item: Item) => {
+      if (item.id.toString() === '00000000-0000-0000-0000-000000000000') {
         return item1Projection
         return item1Projection
-      } else if (item.uuid === '2-3-4') {
+      } else if (item.id.toString() === '00000000-0000-0000-0000-000000000001') {
         return item2Projection
         return item2Projection
       }
       }
 
 
       return undefined
       return undefined
     })
     })
 
 
-    item1 = {
-      uuid: '1-2-3',
-      updatedAtTimestamp: 100,
-    } as jest.Mocked<Item>
+    item1 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
 
 
-    item2 = {
-      uuid: '2-3-4',
-    } as jest.Mocked<Item>
+    item2 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
+    ).getValue()
   })
   })
 
 
   it('should turn sync items response into a sync response for API Version 20161215', async () => {
   it('should turn sync items response into a sync response for API Version 20161215', async () => {
@@ -83,10 +108,18 @@ describe('SyncResponseFactory20161215', () => {
   it('should pick out conflicts between saved and retrieved items and remove them from the later', async () => {
   it('should pick out conflicts between saved and retrieved items and remove them from the later', async () => {
     const itemHash1 = {} as jest.Mocked<ItemHash>
     const itemHash1 = {} as jest.Mocked<ItemHash>
 
 
-    const duplicateItem1 = Object.assign({}, item1)
-    duplicateItem1.updatedAtTimestamp = item1.updatedAtTimestamp + 21_000_000
+    const duplicateItem1 = Item.create(
+      {
+        ...item1.props,
+        timestamps: Timestamps.create(
+          item1.props.timestamps.createdAt,
+          item1.props.timestamps.updatedAt + 21_000_000,
+        ).getValue(),
+      },
+      item1.id,
+    ).getValue()
 
 
-    const duplicateItem2 = Object.assign({}, item2)
+    const duplicateItem2 = Item.create({ ...item2.props }).getValue()
 
 
     expect(
     expect(
       await createFactory().createResponse({
       await createFactory().createResponse({

+ 12 - 14
packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts

@@ -1,18 +1,18 @@
 import { ConflictType } from '@standardnotes/responses'
 import { ConflictType } from '@standardnotes/responses'
+import { MapperInterface } from '@standardnotes/domain-core'
 
 
-import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
 import { Item } from '../Item'
 import { Item } from '../Item'
 import { ItemConflict } from '../ItemConflict'
 import { ItemConflict } from '../ItemConflict'
 import { ItemHash } from '../ItemHash'
 import { ItemHash } from '../ItemHash'
-import { ItemProjection } from '../../../Projection/ItemProjection'
 import { SyncResponse20161215 } from './SyncResponse20161215'
 import { SyncResponse20161215 } from './SyncResponse20161215'
 import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
 import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
 import { SyncItemsResponse } from '../../UseCase/Syncing/SyncItems/SyncItemsResponse'
 import { SyncItemsResponse } from '../../UseCase/Syncing/SyncItems/SyncItemsResponse'
+import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
 
 
 export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface {
 export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface {
   private readonly LEGACY_MIN_CONFLICT_INTERVAL = 20_000_000
   private readonly LEGACY_MIN_CONFLICT_INTERVAL = 20_000_000
 
 
-  constructor(private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
+  constructor(private mapper: MapperInterface<Item, ItemHttpRepresentation>) {}
 
 
   async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20161215> {
   async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20161215> {
     const conflicts = syncItemsResponse.conflicts.filter(
     const conflicts = syncItemsResponse.conflicts.filter(
@@ -28,9 +28,7 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface
     const unsaved = []
     const unsaved = []
     for (const conflict of pickOutConflictsResult.unsavedItems) {
     for (const conflict of pickOutConflictsResult.unsavedItems) {
       unsaved.push({
       unsaved.push({
-        item: conflict.serverItem
-          ? <ItemProjection>await this.itemProjector.projectFull(conflict.serverItem)
-          : <ItemHash>conflict.unsavedItem,
+        item: conflict.serverItem ? this.mapper.toProjection(conflict.serverItem) : <ItemHash>conflict.unsavedItem,
         error: {
         error: {
           tag: conflict.type,
           tag: conflict.type,
         },
         },
@@ -39,12 +37,12 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface
 
 
     const retrievedItems = []
     const retrievedItems = []
     for (const item of pickOutConflictsResult.retrievedItems) {
     for (const item of pickOutConflictsResult.retrievedItems) {
-      retrievedItems.push(<ItemProjection>await this.itemProjector.projectFull(item))
+      retrievedItems.push(this.mapper.toProjection(item))
     }
     }
 
 
     const savedItems = []
     const savedItems = []
     for (const item of syncItemsResponse.savedItems) {
     for (const item of syncItemsResponse.savedItems) {
-      savedItems.push(<ItemProjection>await this.itemProjector.projectFull(item))
+      savedItems.push(this.mapper.toProjection(item))
     }
     }
 
 
     return {
     return {
@@ -64,16 +62,16 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface
     unsavedItems: Array<ItemConflict>
     unsavedItems: Array<ItemConflict>
     retrievedItems: Array<Item>
     retrievedItems: Array<Item>
   } {
   } {
-    const savedIds: Array<string> = savedItems.map((savedItem: Item) => savedItem.uuid)
-    const retrievedIds: Array<string> = retrievedItems.map((retrievedItem: Item) => retrievedItem.uuid)
+    const savedIds: Array<string> = savedItems.map((savedItem: Item) => savedItem.id.toString())
+    const retrievedIds: Array<string> = retrievedItems.map((retrievedItem: Item) => retrievedItem.id.toString())
 
 
     const conflictingIds = savedIds.filter((savedId) => retrievedIds.includes(savedId))
     const conflictingIds = savedIds.filter((savedId) => retrievedIds.includes(savedId))
 
 
     for (const conflictingId of conflictingIds) {
     for (const conflictingId of conflictingIds) {
-      const savedItem = <Item>savedItems.find((item) => item.uuid === conflictingId)
-      const conflictedItem = <Item>retrievedItems.find((item) => item.uuid === conflictingId)
+      const savedItem = <Item>savedItems.find((item) => item.id.toString() === conflictingId)
+      const conflictedItem = <Item>retrievedItems.find((item) => item.id.toString() === conflictingId)
 
 
-      const difference = savedItem.updatedAtTimestamp - conflictedItem.updatedAtTimestamp
+      const difference = savedItem.props.timestamps.updatedAt - conflictedItem.props.timestamps.updatedAt
 
 
       if (Math.abs(difference) > this.LEGACY_MIN_CONFLICT_INTERVAL) {
       if (Math.abs(difference) > this.LEGACY_MIN_CONFLICT_INTERVAL) {
         unsavedItems.push({
         unsavedItems.push({
@@ -82,7 +80,7 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface
         })
         })
       }
       }
 
 
-      retrievedItems = retrievedItems.filter((retrievedItem: Item) => retrievedItem.uuid !== conflictingId)
+      retrievedItems = retrievedItems.filter((retrievedItem: Item) => retrievedItem.id.toString() !== conflictingId)
     }
     }
 
 
     return {
     return {

+ 23 - 22
packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts

@@ -1,43 +1,44 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
-import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
+
+import { MapperInterface } from '@standardnotes/domain-core'
+
 import { Item } from '../Item'
 import { Item } from '../Item'
 import { ItemConflict } from '../ItemConflict'
 import { ItemConflict } from '../ItemConflict'
-import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection'
-import { ItemProjection } from '../../../Projection/ItemProjection'
-
 import { SyncResponseFactory20200115 } from './SyncResponseFactory20200115'
 import { SyncResponseFactory20200115 } from './SyncResponseFactory20200115'
-import { SavedItemProjection } from '../../../Projection/SavedItemProjection'
+import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
+import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation'
+import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation'
 
 
 describe('SyncResponseFactory20200115', () => {
 describe('SyncResponseFactory20200115', () => {
-  let itemProjector: ProjectorInterface<Item, ItemProjection>
-  let savedItemProjector: ProjectorInterface<Item, SavedItemProjection>
-  let itemConflictProjector: ProjectorInterface<ItemConflict, ItemConflictProjection>
-  let itemProjection: ItemProjection
-  let savedItemProjection: SavedItemProjection
-  let itemConflictProjection: ItemConflictProjection
+  let itemMapper: MapperInterface<Item, ItemHttpRepresentation>
+  let savedItemMapper: MapperInterface<Item, SavedItemHttpRepresentation>
+  let itemConflictMapper: MapperInterface<ItemConflict, ItemConflictHttpRepresentation>
+  let itemProjection: ItemHttpRepresentation
+  let savedItemHttpRepresentation: SavedItemHttpRepresentation
+  let itemConflictProjection: ItemConflictHttpRepresentation
   let item1: Item
   let item1: Item
   let item2: Item
   let item2: Item
   let itemConflict: ItemConflict
   let itemConflict: ItemConflict
 
 
-  const createFactory = () => new SyncResponseFactory20200115(itemProjector, itemConflictProjector, savedItemProjector)
+  const createFactory = () => new SyncResponseFactory20200115(itemMapper, itemConflictMapper, savedItemMapper)
 
 
   beforeEach(() => {
   beforeEach(() => {
     itemProjection = {
     itemProjection = {
       uuid: '2-3-4',
       uuid: '2-3-4',
-    } as jest.Mocked<ItemProjection>
+    } as jest.Mocked<ItemHttpRepresentation>
 
 
-    itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
-    itemProjector.projectFull = jest.fn().mockReturnValue(itemProjection)
+    itemMapper = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
+    itemMapper.toProjection = jest.fn().mockReturnValue(itemProjection)
 
 
-    itemConflictProjector = {} as jest.Mocked<ProjectorInterface<ItemConflict, ItemConflictProjection>>
-    itemConflictProjector.projectFull = jest.fn().mockReturnValue(itemConflictProjection)
+    itemConflictMapper = {} as jest.Mocked<MapperInterface<ItemConflict, ItemConflictHttpRepresentation>>
+    itemConflictMapper.toProjection = jest.fn().mockReturnValue(itemConflictProjection)
 
 
-    savedItemProjection = {
+    savedItemHttpRepresentation = {
       uuid: '1-2-3',
       uuid: '1-2-3',
-    } as jest.Mocked<SavedItemProjection>
+    } as jest.Mocked<SavedItemHttpRepresentation>
 
 
-    savedItemProjector = {} as jest.Mocked<ProjectorInterface<Item, SavedItemProjection>>
-    savedItemProjector.projectFull = jest.fn().mockReturnValue(savedItemProjection)
+    savedItemMapper = {} as jest.Mocked<MapperInterface<Item, SavedItemHttpRepresentation>>
+    savedItemMapper.toProjection = jest.fn().mockReturnValue(savedItemHttpRepresentation)
 
 
     item1 = {} as jest.Mocked<Item>
     item1 = {} as jest.Mocked<Item>
 
 
@@ -57,7 +58,7 @@ describe('SyncResponseFactory20200115', () => {
       }),
       }),
     ).toEqual({
     ).toEqual({
       retrieved_items: [itemProjection],
       retrieved_items: [itemProjection],
-      saved_items: [savedItemProjection],
+      saved_items: [savedItemHttpRepresentation],
       conflicts: [itemConflictProjection],
       conflicts: [itemConflictProjection],
       sync_token: 'sync-test',
       sync_token: 'sync-test',
       cursor_token: 'cursor-test',
       cursor_token: 'cursor-test',

+ 11 - 10
packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts

@@ -1,34 +1,35 @@
-import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
+import { MapperInterface } from '@standardnotes/domain-core'
+
 import { Item } from '../Item'
 import { Item } from '../Item'
 import { ItemConflict } from '../ItemConflict'
 import { ItemConflict } from '../ItemConflict'
-import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection'
-import { ItemProjection } from '../../../Projection/ItemProjection'
 import { SyncResponse20200115 } from './SyncResponse20200115'
 import { SyncResponse20200115 } from './SyncResponse20200115'
 import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
 import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
-import { SavedItemProjection } from '../../../Projection/SavedItemProjection'
 import { SyncItemsResponse } from '../../UseCase/Syncing/SyncItems/SyncItemsResponse'
 import { SyncItemsResponse } from '../../UseCase/Syncing/SyncItems/SyncItemsResponse'
+import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
+import { ItemConflictHttpRepresentation } from '../../../Mapping/Http/ItemConflictHttpRepresentation'
+import { SavedItemHttpRepresentation } from '../../../Mapping/Http/SavedItemHttpRepresentation'
 
 
 export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface {
 export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface {
   constructor(
   constructor(
-    private itemProjector: ProjectorInterface<Item, ItemProjection>,
-    private itemConflictProjector: ProjectorInterface<ItemConflict, ItemConflictProjection>,
-    private savedItemProjector: ProjectorInterface<Item, SavedItemProjection>,
+    private httpMapper: MapperInterface<Item, ItemHttpRepresentation>,
+    private itemConflictMapper: MapperInterface<ItemConflict, ItemConflictHttpRepresentation>,
+    private savedItemMapper: MapperInterface<Item, SavedItemHttpRepresentation>,
   ) {}
   ) {}
 
 
   async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {
   async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {
     const retrievedItems = []
     const retrievedItems = []
     for (const item of syncItemsResponse.retrievedItems) {
     for (const item of syncItemsResponse.retrievedItems) {
-      retrievedItems.push(<ItemProjection>await this.itemProjector.projectFull(item))
+      retrievedItems.push(this.httpMapper.toProjection(item))
     }
     }
 
 
     const savedItems = []
     const savedItems = []
     for (const item of syncItemsResponse.savedItems) {
     for (const item of syncItemsResponse.savedItems) {
-      savedItems.push(<SavedItemProjection>await this.savedItemProjector.projectFull(item))
+      savedItems.push(this.savedItemMapper.toProjection(item))
     }
     }
 
 
     const conflicts = []
     const conflicts = []
     for (const itemConflict of syncItemsResponse.conflicts) {
     for (const itemConflict of syncItemsResponse.conflicts) {
-      conflicts.push(<ItemConflictProjection>await this.itemConflictProjector.projectFull(itemConflict))
+      conflicts.push(this.itemConflictMapper.toProjection(itemConflict))
     }
     }
 
 
     return {
     return {

+ 7 - 6
packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.spec.ts

@@ -1,9 +1,10 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
 
 
+import { ContentType } from '@standardnotes/domain-core'
+
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 
 
 import { CheckIntegrity } from './CheckIntegrity'
 import { CheckIntegrity } from './CheckIntegrity'
-import { ContentType } from '@standardnotes/common'
 
 
 describe('CheckIntegrity', () => {
 describe('CheckIntegrity', () => {
   let itemRepository: ItemRepositoryInterface
   let itemRepository: ItemRepositoryInterface
@@ -16,27 +17,27 @@ describe('CheckIntegrity', () => {
       {
       {
         uuid: '1-2-3',
         uuid: '1-2-3',
         updated_at_timestamp: 1,
         updated_at_timestamp: 1,
-        content_type: ContentType.Note,
+        content_type: ContentType.TYPES.Note,
       },
       },
       {
       {
         uuid: '2-3-4',
         uuid: '2-3-4',
         updated_at_timestamp: 2,
         updated_at_timestamp: 2,
-        content_type: ContentType.Note,
+        content_type: ContentType.TYPES.Note,
       },
       },
       {
       {
         uuid: '3-4-5',
         uuid: '3-4-5',
         updated_at_timestamp: 3,
         updated_at_timestamp: 3,
-        content_type: ContentType.Note,
+        content_type: ContentType.TYPES.Note,
       },
       },
       {
       {
         uuid: '4-5-6',
         uuid: '4-5-6',
         updated_at_timestamp: 4,
         updated_at_timestamp: 4,
-        content_type: ContentType.ItemsKey,
+        content_type: ContentType.TYPES.ItemsKey,
       },
       },
       {
       {
         uuid: '5-6-7',
         uuid: '5-6-7',
         updated_at_timestamp: 5,
         updated_at_timestamp: 5,
-        content_type: ContentType.File,
+        content_type: ContentType.TYPES.File,
       },
       },
     ])
     ])
   })
   })

+ 3 - 4
packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.ts

@@ -1,6 +1,5 @@
 import { IntegrityPayload } from '@standardnotes/responses'
 import { IntegrityPayload } from '@standardnotes/responses'
-import { Result, UseCaseInterface } from '@standardnotes/domain-core'
-import { ContentType } from '@standardnotes/common'
+import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core'
 
 
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { CheckIntegrityDTO } from './CheckIntegrityDTO'
 import { CheckIntegrityDTO } from './CheckIntegrityDTO'
@@ -33,7 +32,7 @@ export class CheckIntegrity implements UseCaseInterface<IntegrityPayload[]> {
       ) as ExtendedIntegrityPayload
       ) as ExtendedIntegrityPayload
 
 
       if (!clientItemIntegrityPayloadsMap.has(serverItemIntegrityPayloadUuid)) {
       if (!clientItemIntegrityPayloadsMap.has(serverItemIntegrityPayloadUuid)) {
-        if (serverItemIntegrityPayload.content_type !== ContentType.ItemsKey) {
+        if (serverItemIntegrityPayload.content_type !== ContentType.TYPES.ItemsKey) {
           mismatches.unshift({
           mismatches.unshift({
             uuid: serverItemIntegrityPayloadUuid,
             uuid: serverItemIntegrityPayloadUuid,
             updated_at_timestamp: serverItemIntegrityPayload.updated_at_timestamp,
             updated_at_timestamp: serverItemIntegrityPayload.updated_at_timestamp,
@@ -49,7 +48,7 @@ export class CheckIntegrity implements UseCaseInterface<IntegrityPayload[]> {
       ) as number
       ) as number
       if (
       if (
         serverItemIntegrityPayloadUpdatedAtTimestamp !== clientItemIntegrityPayloadUpdatedAtTimestamp &&
         serverItemIntegrityPayloadUpdatedAtTimestamp !== clientItemIntegrityPayloadUpdatedAtTimestamp &&
-        serverItemIntegrityPayload.content_type !== ContentType.ItemsKey
+        serverItemIntegrityPayload.content_type !== ContentType.TYPES.ItemsKey
       ) {
       ) {
         mismatches.unshift({
         mismatches.unshift({
           uuid: serverItemIntegrityPayloadUuid,
           uuid: serverItemIntegrityPayloadUuid,

+ 264 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.spec.ts

@@ -0,0 +1,264 @@
+import { Timer, TimerInterface } from '@standardnotes/time'
+import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
+import { SaveNewItem } from './SaveNewItem'
+import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+import { ItemHash } from '../../../Item/ItemHash'
+import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
+import { Item } from '../../../Item/Item'
+
+describe('SaveNewItem', () => {
+  let itemRepository: ItemRepositoryInterface
+  let timer: TimerInterface
+  let domainEventPublisher: DomainEventPublisherInterface
+  let domainEventFactory: DomainEventFactoryInterface
+  let itemHash1: ItemHash
+  let item1: Item
+
+  const createUseCase = () => new SaveNewItem(itemRepository, timer, domainEventPublisher, domainEventFactory)
+
+  beforeEach(() => {
+    const timeHelper = new Timer()
+
+    item1 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
+
+    itemHash1 = {
+      uuid: '1-2-3',
+      content: 'asdqwe1',
+      content_type: ContentType.TYPES.Note,
+      duplicate_of: null,
+      enc_item_key: 'qweqwe1',
+      items_key_id: 'asdasd1',
+      created_at: timeHelper.formatDate(
+        timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
+        'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
+      ),
+      updated_at: timeHelper.formatDate(
+        new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
+        'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
+      ),
+    } as jest.Mocked<ItemHash>
+
+    itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
+    itemRepository.save = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
+    timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
+    timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(123456789)
+    timer.convertStringDateToDate = jest.fn().mockReturnValue(new Date(123456789))
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createDuplicateItemSyncedEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
+    domainEventFactory.createItemRevisionCreationRequested = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
+  })
+
+  it('saves a new item', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(itemRepository.save).toHaveBeenCalled()
+  })
+
+  it('saves a new empty item', async () => {
+    const useCase = createUseCase()
+
+    itemHash1.content = undefined
+    itemHash1.content_type = null
+    itemHash1.enc_item_key = undefined
+    itemHash1.items_key_id = undefined
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(itemRepository.save).toHaveBeenCalled()
+  })
+
+  it('saves a new item with given timestamps', async () => {
+    const useCase = createUseCase()
+
+    itemHash1.created_at_timestamp = 123
+    itemHash1.updated_at_timestamp = 123
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+  })
+
+  it('publishes a duplicate item synced event if the item is a duplicate', async () => {
+    const useCase = createUseCase()
+
+    itemHash1.duplicate_of = '00000000-0000-0000-0000-000000000003'
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalled()
+    expect(domainEventPublisher.publish).toHaveBeenCalled()
+  })
+
+  it('publishes a item revision creation requested event if the item is a revision', async () => {
+    const useCase = createUseCase()
+
+    itemHash1.updated_at = '2021-03-19T17:17:13.241Z'
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalled()
+    expect(domainEventPublisher.publish).toHaveBeenCalled()
+  })
+
+  it('returns a failure if the item cannot be saved', async () => {
+    const mock = jest.spyOn(Item, 'create')
+    mock.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+
+    mock.mockRestore()
+  })
+
+  it('returns a failure if the user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-00000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('returns a failure if the session uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-00000000000',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('returns a failure if the content type is invalid', async () => {
+    const useCase = createUseCase()
+
+    itemHash1.content_type = 'invalid'
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('returns a failure if the duplicate uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    itemHash1.duplicate_of = 'invalid'
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('returns a failure if it fails to create dates', async () => {
+    const mock = jest.spyOn(Dates, 'create')
+    mock.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+
+    mock.mockRestore()
+  })
+
+  it('return a failure if it fails to create timestamps', async () => {
+    const mock = jest.spyOn(Timestamps, 'create')
+    mock.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+
+    mock.mockRestore()
+  })
+})

+ 121 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts

@@ -0,0 +1,121 @@
+import {
+  ContentType,
+  Dates,
+  Result,
+  Timestamps,
+  UniqueEntityId,
+  UseCaseInterface,
+  Uuid,
+} from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
+
+import { Item } from '../../../Item/Item'
+import { SaveNewItemDTO } from './SaveNewItemDTO'
+import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+
+export class SaveNewItem implements UseCaseInterface<Item> {
+  constructor(
+    private itemRepository: ItemRepositoryInterface,
+    private timer: TimerInterface,
+    private domainEventPublisher: DomainEventPublisherInterface,
+    private domainEventFactory: DomainEventFactoryInterface,
+  ) {}
+
+  async execute(dto: SaveNewItemDTO): Promise<Result<Item>> {
+    let updatedWithSession = null
+    if (dto.sessionUuid) {
+      const sessionUuidOrError = Uuid.create(dto.sessionUuid)
+      if (sessionUuidOrError.isFailed()) {
+        return Result.fail(sessionUuidOrError.getError())
+      }
+      updatedWithSession = sessionUuidOrError.getValue()
+    }
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
+    if (contentTypeOrError.isFailed()) {
+      return Result.fail(contentTypeOrError.getError())
+    }
+    const contentType = contentTypeOrError.getValue()
+
+    let duplicateOf = null
+    if (dto.itemHash.duplicate_of) {
+      const duplicateOfOrError = Uuid.create(dto.itemHash.duplicate_of)
+      if (duplicateOfOrError.isFailed()) {
+        return Result.fail(duplicateOfOrError.getError())
+      }
+      duplicateOf = duplicateOfOrError.getValue()
+    }
+
+    const now = this.timer.getTimestampInMicroseconds()
+    const nowDate = this.timer.convertMicrosecondsToDate(now)
+
+    let createdAtDate = nowDate
+    let createdAtTimestamp = now
+    if (dto.itemHash.created_at_timestamp) {
+      createdAtTimestamp = dto.itemHash.created_at_timestamp
+      createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
+    } else if (dto.itemHash.created_at) {
+      createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
+      createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.created_at)
+    }
+
+    const datesOrError = Dates.create(createdAtDate, nowDate)
+    if (datesOrError.isFailed()) {
+      return Result.fail(datesOrError.getError())
+    }
+    const dates = datesOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(createdAtTimestamp, now)
+    if (timestampsOrError.isFailed()) {
+      return Result.fail(timestampsOrError.getError())
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    const itemOrError = Item.create(
+      {
+        updatedWithSession,
+        content: dto.itemHash.content ?? null,
+        userUuid,
+        contentType,
+        encItemKey: dto.itemHash.enc_item_key ?? null,
+        authHash: dto.itemHash.auth_hash ?? null,
+        itemsKeyId: dto.itemHash.items_key_id ?? null,
+        duplicateOf,
+        deleted: dto.itemHash.deleted ?? false,
+        dates,
+        timestamps,
+      },
+      new UniqueEntityId(dto.itemHash.uuid),
+    )
+    if (itemOrError.isFailed()) {
+      return Result.fail(itemOrError.getError())
+    }
+    const newItem = itemOrError.getValue()
+
+    await this.itemRepository.save(newItem)
+
+    if (contentType.value !== null && [ContentType.TYPES.Note, ContentType.TYPES.File].includes(contentType.value)) {
+      await this.domainEventPublisher.publish(
+        this.domainEventFactory.createItemRevisionCreationRequested(
+          newItem.id.toString(),
+          newItem.props.userUuid.value,
+        ),
+      )
+    }
+
+    if (duplicateOf) {
+      await this.domainEventPublisher.publish(
+        this.domainEventFactory.createDuplicateItemSyncedEvent(newItem.id.toString(), newItem.props.userUuid.value),
+      )
+    }
+
+    return Result.ok(newItem)
+  }
+}

+ 7 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItemDTO.ts

@@ -0,0 +1,7 @@
+import { ItemHash } from '../../../Item/ItemHash'
+
+export interface SaveNewItemDTO {
+  userUuid: string
+  itemHash: ItemHash
+  sessionUuid: string | null
+}

+ 50 - 12
packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts

@@ -1,13 +1,12 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
 
 
-import { ContentType } from '@standardnotes/common'
-
 import { ApiVersion } from '../../../Api/ApiVersion'
 import { ApiVersion } from '../../../Api/ApiVersion'
 import { Item } from '../../../Item/Item'
 import { Item } from '../../../Item/Item'
 import { ItemHash } from '../../../Item/ItemHash'
 import { ItemHash } from '../../../Item/ItemHash'
 import { ItemServiceInterface } from '../../../Item/ItemServiceInterface'
 import { ItemServiceInterface } from '../../../Item/ItemServiceInterface'
 
 
 import { SyncItems } from './SyncItems'
 import { SyncItems } from './SyncItems'
+import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
 
 
 describe('SyncItems', () => {
 describe('SyncItems', () => {
   let itemService: ItemServiceInterface
   let itemService: ItemServiceInterface
@@ -19,20 +18,59 @@ describe('SyncItems', () => {
   const createUseCase = () => new SyncItems(itemService)
   const createUseCase = () => new SyncItems(itemService)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    item1 = {
-      uuid: '1-2-3',
-    } as jest.Mocked<Item>
-    item2 = {
-      uuid: '2-3-4',
-    } as jest.Mocked<Item>
-    item3 = {
-      uuid: '3-4-5',
-    } as jest.Mocked<Item>
+    item1 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
+    ).getValue()
+    item2 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
+    ).getValue()
+    item3 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
+    ).getValue()
 
 
     itemHash = {
     itemHash = {
       uuid: '2-3-4',
       uuid: '2-3-4',
       content: 'asdqwe',
       content: 'asdqwe',
-      content_type: ContentType.Note,
+      content_type: ContentType.TYPES.Note,
       duplicate_of: null,
       duplicate_of: null,
       enc_item_key: 'qweqwe',
       enc_item_key: 'qweqwe',
       items_key_id: 'asdasd',
       items_key_id: 'asdasd',

+ 3 - 2
packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts

@@ -1,4 +1,5 @@
 import { Result, UseCaseInterface } from '@standardnotes/domain-core'
 import { Result, UseCaseInterface } from '@standardnotes/domain-core'
+
 import { Item } from '../../../Item/Item'
 import { Item } from '../../../Item/Item'
 import { ItemConflict } from '../../../Item/ItemConflict'
 import { ItemConflict } from '../../../Item/ItemConflict'
 import { ItemServiceInterface } from '../../../Item/ItemServiceInterface'
 import { ItemServiceInterface } from '../../../Item/ItemServiceInterface'
@@ -52,10 +53,10 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
     const syncConflictIds: Array<string> = []
     const syncConflictIds: Array<string> = []
     conflicts.forEach((conflict: ItemConflict) => {
     conflicts.forEach((conflict: ItemConflict) => {
       if (conflict.type === 'sync_conflict' && conflict.serverItem) {
       if (conflict.type === 'sync_conflict' && conflict.serverItem) {
-        syncConflictIds.push(conflict.serverItem.uuid)
+        syncConflictIds.push(conflict.serverItem.id.toString())
       }
       }
     })
     })
 
 
-    return retrievedItems.filter((item: Item) => syncConflictIds.indexOf(item.uuid) === -1)
+    return retrievedItems.filter((item: Item) => syncConflictIds.indexOf(item.id.toString()) === -1)
   }
   }
 }
 }

+ 251 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts

@@ -0,0 +1,251 @@
+import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { Timer, TimerInterface } from '@standardnotes/time'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+import { Item } from '../../../Item/Item'
+import { ItemHash } from '../../../Item/ItemHash'
+import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
+import { UpdateExistingItem } from './UpdateExistingItem'
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
+
+describe('UpdateExistingItem', () => {
+  let itemRepository: ItemRepositoryInterface
+  let timer: TimerInterface
+  let domainEventPublisher: DomainEventPublisherInterface
+  let domainEventFactory: DomainEventFactoryInterface
+  let itemHash1: ItemHash
+  let item1: Item
+
+  const createUseCase = () => new UpdateExistingItem(itemRepository, timer, domainEventPublisher, domainEventFactory, 5)
+
+  beforeEach(() => {
+    const timeHelper = new Timer()
+
+    item1 = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
+
+    itemHash1 = {
+      uuid: '1-2-3',
+      content: 'asdqwe1',
+      content_type: ContentType.TYPES.Note,
+      duplicate_of: null,
+      enc_item_key: 'qweqwe1',
+      auth_hash: 'auth_hash',
+      items_key_id: 'asdasd1',
+      created_at: timeHelper.formatDate(
+        timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
+        'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
+      ),
+      updated_at: timeHelper.formatDate(
+        new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
+        'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
+      ),
+    } as jest.Mocked<ItemHash>
+
+    itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
+    itemRepository.save = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
+    timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
+    timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(123456789)
+    timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(123456789)
+    timer.convertStringDateToDate = jest.fn().mockReturnValue(new Date(123456789))
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createDuplicateItemSyncedEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
+    domainEventFactory.createItemRevisionCreationRequested = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
+  })
+
+  it('should update item', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: itemHash1,
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(itemRepository.save).toHaveBeenCalled()
+  })
+
+  it('should return error if session uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: itemHash1,
+      sessionUuid: 'invalid-uuid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if content type is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        content_type: 'invalid',
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should mark item as deleted if item hash is deleted', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        deleted: true,
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(itemRepository.save).toHaveBeenCalled()
+    expect(item1.props.deleted).toBeTruthy()
+    expect(item1.props.content).toBeNull()
+    expect(item1.props.encItemKey).toBeNull()
+    expect(item1.props.authHash).toBeNull()
+    expect(item1.props.itemsKeyId).toBeNull()
+    expect(item1.props.duplicateOf).toBeNull()
+  })
+
+  it('should mark item as duplicate if item hash has duplicate_of', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        duplicate_of: '00000000-0000-0000-0000-000000000001',
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(itemRepository.save).toHaveBeenCalled()
+    expect(item1.props.duplicateOf?.value).toBe('00000000-0000-0000-0000-000000000001')
+  })
+
+  it('shuld return error if duplicate uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        duplicate_of: 'invalid-uuid',
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should update item with update timestamps', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        updated_at_timestamp: 123,
+        created_at_timestamp: 123,
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(itemRepository.save).toHaveBeenCalled()
+  })
+
+  it('should return error if created at time is not give in any form', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        created_at: undefined,
+        created_at_timestamp: undefined,
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if dates could not be created from timestamps', async () => {
+    const mock = jest.spyOn(Dates, 'create')
+    mock.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        created_at_timestamp: 123,
+        updated_at_timestamp: 123,
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+
+    mock.mockRestore()
+  })
+
+  it('should return error if timestamps could not be created from timestamps', async () => {
+    const mock = jest.spyOn(Timestamps, 'create')
+    mock.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      existingItem: item1,
+      itemHash: {
+        ...itemHash1,
+        created_at_timestamp: 123,
+        updated_at_timestamp: 123,
+      },
+      sessionUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    mock.mockRestore()
+  })
+})

+ 135 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts

@@ -0,0 +1,135 @@
+import { ContentType, Dates, Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { TimerInterface } from '@standardnotes/time'
+
+import { Item } from '../../../Item/Item'
+import { UpdateExistingItemDTO } from './UpdateExistingItemDTO'
+import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+
+export class UpdateExistingItem implements UseCaseInterface<Item> {
+  constructor(
+    private itemRepository: ItemRepositoryInterface,
+    private timer: TimerInterface,
+    private domainEventPublisher: DomainEventPublisherInterface,
+    private domainEventFactory: DomainEventFactoryInterface,
+    private revisionFrequency: number,
+  ) {}
+
+  async execute(dto: UpdateExistingItemDTO): Promise<Result<Item>> {
+    let sessionUuid = null
+    if (dto.sessionUuid) {
+      const sessionUuidOrError = Uuid.create(dto.sessionUuid)
+      if (sessionUuidOrError.isFailed()) {
+        return Result.fail(sessionUuidOrError.getError())
+      }
+      sessionUuid = sessionUuidOrError.getValue()
+    }
+    dto.existingItem.props.updatedWithSession = sessionUuid
+
+    if (dto.itemHash.content) {
+      dto.existingItem.props.content = dto.itemHash.content
+    }
+
+    if (dto.itemHash.content_type) {
+      const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
+      if (contentTypeOrError.isFailed()) {
+        return Result.fail(contentTypeOrError.getError())
+      }
+      const contentType = contentTypeOrError.getValue()
+      dto.existingItem.props.contentType = contentType
+    }
+
+    if (dto.itemHash.deleted !== undefined) {
+      dto.existingItem.props.deleted = dto.itemHash.deleted
+    }
+
+    let wasMarkedAsDuplicate = false
+    if (dto.itemHash.duplicate_of) {
+      const duplicateOfOrError = Uuid.create(dto.itemHash.duplicate_of)
+      if (duplicateOfOrError.isFailed()) {
+        return Result.fail(duplicateOfOrError.getError())
+      }
+      wasMarkedAsDuplicate = dto.existingItem.props.duplicateOf === null
+      dto.existingItem.props.duplicateOf = duplicateOfOrError.getValue()
+    }
+
+    if (dto.itemHash.auth_hash) {
+      dto.existingItem.props.authHash = dto.itemHash.auth_hash
+    }
+    if (dto.itemHash.enc_item_key) {
+      dto.existingItem.props.encItemKey = dto.itemHash.enc_item_key
+    }
+    if (dto.itemHash.items_key_id) {
+      dto.existingItem.props.itemsKeyId = dto.itemHash.items_key_id
+    }
+
+    const updatedAtTimestamp = this.timer.getTimestampInMicroseconds()
+    const secondsFromLastUpdate = this.timer.convertMicrosecondsToSeconds(
+      updatedAtTimestamp - dto.existingItem.props.timestamps.updatedAt,
+    )
+    const updatedAtDate = this.timer.convertMicrosecondsToDate(updatedAtTimestamp)
+
+    let createdAtTimestamp: number
+    let createdAtDate: Date
+    if (dto.itemHash.created_at_timestamp) {
+      createdAtTimestamp = dto.itemHash.created_at_timestamp
+      createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
+    } else if (dto.itemHash.created_at) {
+      createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
+      createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.created_at)
+    } else {
+      return Result.fail('Created at timestamp is required.')
+    }
+
+    const datesOrError = Dates.create(createdAtDate, updatedAtDate)
+    if (datesOrError.isFailed()) {
+      return Result.fail(datesOrError.getError())
+    }
+    dto.existingItem.props.dates = datesOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(createdAtTimestamp, updatedAtTimestamp)
+    if (timestampsOrError.isFailed()) {
+      return Result.fail(timestampsOrError.getError())
+    }
+    dto.existingItem.props.timestamps = timestampsOrError.getValue()
+
+    dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
+
+    if (dto.itemHash.deleted === true) {
+      dto.existingItem.props.deleted = true
+      dto.existingItem.props.content = null
+      dto.existingItem.props.contentSize = 0
+      dto.existingItem.props.encItemKey = null
+      dto.existingItem.props.authHash = null
+      dto.existingItem.props.itemsKeyId = null
+    }
+
+    await this.itemRepository.save(dto.existingItem)
+
+    if (secondsFromLastUpdate >= this.revisionFrequency) {
+      if (
+        dto.existingItem.props.contentType.value !== null &&
+        [ContentType.TYPES.Note, ContentType.TYPES.File].includes(dto.existingItem.props.contentType.value)
+      ) {
+        await this.domainEventPublisher.publish(
+          this.domainEventFactory.createItemRevisionCreationRequested(
+            dto.existingItem.id.toString(),
+            dto.existingItem.props.userUuid.value,
+          ),
+        )
+      }
+    }
+
+    if (wasMarkedAsDuplicate) {
+      await this.domainEventPublisher.publish(
+        this.domainEventFactory.createDuplicateItemSyncedEvent(
+          dto.existingItem.id.toString(),
+          dto.existingItem.props.userUuid.value,
+        ),
+      )
+    }
+
+    return Result.ok(dto.existingItem)
+  }
+}

+ 8 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItemDTO.ts

@@ -0,0 +1,8 @@
+import { Item } from '../../../Item/Item'
+import { ItemHash } from '../../../Item/ItemHash'
+
+export interface UpdateExistingItemDTO {
+  existingItem: Item
+  itemHash: ItemHash
+  sessionUuid: string | null
+}

+ 5 - 5
packages/syncing-server/src/Infra/FS/FSItemBackupService.ts

@@ -1,4 +1,5 @@
 import { KeyParamsData } from '@standardnotes/responses'
 import { KeyParamsData } from '@standardnotes/responses'
+import { MapperInterface } from '@standardnotes/domain-core'
 import { promises } from 'fs'
 import { promises } from 'fs'
 import * as uuid from 'uuid'
 import * as uuid from 'uuid'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
@@ -6,13 +7,12 @@ import { dirname } from 'path'
 
 
 import { Item } from '../../Domain/Item/Item'
 import { Item } from '../../Domain/Item/Item'
 import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
 import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
-import { ItemProjection } from '../../Projection/ItemProjection'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
+import { ItemBackupRepresentation } from '../../Mapping/Backup/ItemBackupRepresentation'
 
 
 export class FSItemBackupService implements ItemBackupServiceInterface {
 export class FSItemBackupService implements ItemBackupServiceInterface {
   constructor(
   constructor(
     private fileUploadPath: string,
     private fileUploadPath: string,
-    private itemProjector: ProjectorInterface<Item, ItemProjection>,
+    private mapper: MapperInterface<Item, ItemBackupRepresentation>,
     private logger: Logger,
     private logger: Logger,
   ) {}
   ) {}
 
 
@@ -22,12 +22,12 @@ export class FSItemBackupService implements ItemBackupServiceInterface {
 
 
   async dump(item: Item): Promise<string> {
   async dump(item: Item): Promise<string> {
     const contents = JSON.stringify({
     const contents = JSON.stringify({
-      item: await this.itemProjector.projectCustom('dump', item),
+      item: this.mapper.toProjection(item),
     })
     })
 
 
     const path = `${this.fileUploadPath}/dumps/${uuid.v4()}`
     const path = `${this.fileUploadPath}/dumps/${uuid.v4()}`
 
 
-    this.logger.debug(`Dumping item ${item.uuid} to ${path}`)
+    this.logger.debug(`Dumping item ${item.id.toString()} to ${path}`)
 
 
     await promises.mkdir(dirname(path), { recursive: true })
     await promises.mkdir(dirname(path), { recursive: true })
 
 

+ 4 - 5
packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController.ts

@@ -1,4 +1,4 @@
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
 import { BaseHttpController, results } from 'inversify-express-utils'
 import { BaseHttpController, results } from 'inversify-express-utils'
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 
 
@@ -6,18 +6,17 @@ import { Item } from '../../../Domain/Item/Item'
 import { SyncResponseFactoryResolverInterface } from '../../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
 import { SyncResponseFactoryResolverInterface } from '../../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
 import { CheckIntegrity } from '../../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
 import { CheckIntegrity } from '../../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
 import { GetItem } from '../../../Domain/UseCase/Syncing/GetItem/GetItem'
 import { GetItem } from '../../../Domain/UseCase/Syncing/GetItem/GetItem'
-import { ItemProjection } from '../../../Projection/ItemProjection'
-import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
 import { ApiVersion } from '../../../Domain/Api/ApiVersion'
 import { ApiVersion } from '../../../Domain/Api/ApiVersion'
 import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { HttpStatusCode } from '@standardnotes/responses'
 import { HttpStatusCode } from '@standardnotes/responses'
+import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
 
 
 export class HomeServerItemsController extends BaseHttpController {
 export class HomeServerItemsController extends BaseHttpController {
   constructor(
   constructor(
     protected syncItems: SyncItems,
     protected syncItems: SyncItems,
     protected checkIntegrity: CheckIntegrity,
     protected checkIntegrity: CheckIntegrity,
     protected getItem: GetItem,
     protected getItem: GetItem,
-    protected itemProjector: ProjectorInterface<Item, ItemProjection>,
+    protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
     protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
     protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
     private controllerContainer?: ControllerContainerInterface,
     private controllerContainer?: ControllerContainerInterface,
   ) {
   ) {
@@ -92,6 +91,6 @@ export class HomeServerItemsController extends BaseHttpController {
       return this.json({ error: { message: result.getError() } }, 404)
       return this.json({ error: { message: result.getError() } }, 404)
     }
     }
 
 
-    return this.json({ item: await this.itemProjector.projectFull(result.getValue()) })
+    return this.json({ item: this.itemHttpMapper.toProjection(result.getValue()) })
   }
   }
 }
 }

+ 7 - 9
packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts

@@ -1,14 +1,11 @@
 import 'reflect-metadata'
 import 'reflect-metadata'
 
 
 import * as express from 'express'
 import * as express from 'express'
-import { ContentType } from '@standardnotes/common'
-import { Result } from '@standardnotes/domain-core'
+import { ContentType, MapperInterface, Result } from '@standardnotes/domain-core'
 import { results } from 'inversify-express-utils'
 import { results } from 'inversify-express-utils'
 
 
 import { InversifyExpressItemsController } from './InversifyExpressItemsController'
 import { InversifyExpressItemsController } from './InversifyExpressItemsController'
 import { Item } from '../../Domain/Item/Item'
 import { Item } from '../../Domain/Item/Item'
-import { ItemProjection } from '../../Projection/ItemProjection'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
 import { ApiVersion } from '../../Domain/Api/ApiVersion'
 import { ApiVersion } from '../../Domain/Api/ApiVersion'
 import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115'
 import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115'
 import { SyncResponseFactoryInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryInterface'
 import { SyncResponseFactoryInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryInterface'
@@ -16,12 +13,13 @@ import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResp
 import { CheckIntegrity } from '../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
 import { CheckIntegrity } from '../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
 import { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem'
 import { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem'
 import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
+import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
 
 
 describe('InversifyExpressItemsController', () => {
 describe('InversifyExpressItemsController', () => {
   let syncItems: SyncItems
   let syncItems: SyncItems
   let checkIntegrity: CheckIntegrity
   let checkIntegrity: CheckIntegrity
   let getItem: GetItem
   let getItem: GetItem
-  let itemProjector: ProjectorInterface<Item, ItemProjection>
+  let mapper: MapperInterface<Item, ItemHttpRepresentation>
   let request: express.Request
   let request: express.Request
   let response: express.Response
   let response: express.Response
   let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface
   let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface
@@ -29,11 +27,11 @@ describe('InversifyExpressItemsController', () => {
   let syncResponse: SyncResponse20200115
   let syncResponse: SyncResponse20200115
 
 
   const createController = () =>
   const createController = () =>
-    new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, itemProjector, syncResponceFactoryResolver)
+    new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, mapper, syncResponceFactoryResolver)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
-    itemProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' })
+    mapper = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
+    mapper.toProjection = jest.fn().mockReturnValue({ foo: 'bar' })
 
 
     syncItems = {} as jest.Mocked<SyncItems>
     syncItems = {} as jest.Mocked<SyncItems>
     syncItems.execute = jest.fn().mockReturnValue(Result.ok({ foo: 'bar' }))
     syncItems.execute = jest.fn().mockReturnValue(Result.ok({ foo: 'bar' }))
@@ -58,7 +56,7 @@ describe('InversifyExpressItemsController', () => {
     request.body.items = [
     request.body.items = [
       {
       {
         content: 'test',
         content: 'test',
-        content_type: ContentType.Note,
+        content_type: ContentType.TYPES.Note,
         created_at: '2021-02-19T11:35:45.655Z',
         created_at: '2021-02-19T11:35:45.655Z',
         deleted: false,
         deleted: false,
         duplicate_of: null,
         duplicate_of: null,

+ 4 - 4
packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.ts

@@ -8,9 +8,9 @@ import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResp
 import { CheckIntegrity } from '../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
 import { CheckIntegrity } from '../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
 import { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem'
 import { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem'
 import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
-import { ItemProjection } from '../../Projection/ItemProjection'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
 import { HomeServerItemsController } from './HomeServer/HomeServerItemsController'
 import { HomeServerItemsController } from './HomeServer/HomeServerItemsController'
+import { MapperInterface } from '@standardnotes/domain-core'
+import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
 
 
 @controller('/items', TYPES.Sync_AuthMiddleware)
 @controller('/items', TYPES.Sync_AuthMiddleware)
 export class InversifyExpressItemsController extends HomeServerItemsController {
 export class InversifyExpressItemsController extends HomeServerItemsController {
@@ -18,11 +18,11 @@ export class InversifyExpressItemsController extends HomeServerItemsController {
     @inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
     @inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
     @inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
     @inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
     @inject(TYPES.Sync_GetItem) override getItem: GetItem,
     @inject(TYPES.Sync_GetItem) override getItem: GetItem,
-    @inject(TYPES.Sync_ItemProjector) override itemProjector: ProjectorInterface<Item, ItemProjection>,
+    @inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
     @inject(TYPES.Sync_SyncResponseFactoryResolver)
     @inject(TYPES.Sync_SyncResponseFactoryResolver)
     override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
     override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
   ) {
   ) {
-    super(syncItems, checkIntegrity, getItem, itemProjector, syncResponseFactoryResolver)
+    super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver)
   }
   }
 
 
   @httpPost('/sync')
   @httpPost('/sync')

+ 13 - 8
packages/syncing-server/src/Infra/S3/S3ItemBackupService.ts

@@ -5,13 +5,15 @@ import { Logger } from 'winston'
 
 
 import { Item } from '../../Domain/Item/Item'
 import { Item } from '../../Domain/Item/Item'
 import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
 import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
-import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { ItemProjection } from '../../Projection/ItemProjection'
+import { MapperInterface } from '@standardnotes/domain-core'
+import { ItemBackupRepresentation } from '../../Mapping/Backup/ItemBackupRepresentation'
+import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
 
 
 export class S3ItemBackupService implements ItemBackupServiceInterface {
 export class S3ItemBackupService implements ItemBackupServiceInterface {
   constructor(
   constructor(
     private s3BackupBucketName: string,
     private s3BackupBucketName: string,
-    private itemProjector: ProjectorInterface<Item, ItemProjection>,
+    private backupMapper: MapperInterface<Item, ItemBackupRepresentation>,
+    private httpMapper: MapperInterface<Item, ItemHttpRepresentation>,
     private logger: Logger,
     private logger: Logger,
     private s3Client?: S3Client,
     private s3Client?: S3Client,
   ) {}
   ) {}
@@ -29,7 +31,7 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
         Bucket: this.s3BackupBucketName,
         Bucket: this.s3BackupBucketName,
         Key: s3Key,
         Key: s3Key,
         Body: JSON.stringify({
         Body: JSON.stringify({
-          item: await this.itemProjector.projectCustom('dump', item),
+          item: this.backupMapper.toProjection(item),
         }),
         }),
       }),
       }),
     )
     )
@@ -45,10 +47,10 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
     }
     }
 
 
     const fileNames = []
     const fileNames = []
-    let itemProjections: Array<ItemProjection> = []
+    let itemProjections: Array<ItemHttpRepresentation> = []
     let contentSizeCounter = 0
     let contentSizeCounter = 0
     for (const item of items) {
     for (const item of items) {
-      const itemProjection = await this.itemProjector.projectFull(item)
+      const itemProjection = this.httpMapper.toProjection(item)
 
 
       if (contentSizeLimit === undefined) {
       if (contentSizeLimit === undefined) {
         itemProjections.push(itemProjection)
         itemProjections.push(itemProjection)
@@ -79,7 +81,10 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
     return fileNames
     return fileNames
   }
   }
 
 
-  private async createBackupFile(itemProjections: ItemProjection[], authParams: KeyParamsData): Promise<string> {
+  private async createBackupFile(
+    itemRepresentations: ItemHttpRepresentation[],
+    authParams: KeyParamsData,
+  ): Promise<string> {
     const fileName = uuid.v4()
     const fileName = uuid.v4()
 
 
     await (this.s3Client as S3Client).send(
     await (this.s3Client as S3Client).send(
@@ -87,7 +92,7 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
         Bucket: this.s3BackupBucketName,
         Bucket: this.s3BackupBucketName,
         Key: fileName,
         Key: fileName,
         Body: JSON.stringify({
         Body: JSON.stringify({
-          items: itemProjections,
+          items: itemRepresentations,
           auth_params: authParams,
           auth_params: authParams,
         }),
         }),
       }),
       }),

+ 118 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMItem.ts

@@ -0,0 +1,118 @@
+import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity({ name: 'items' })
+@Index('index_items_on_user_uuid_and_content_type', ['userUuid', 'contentType'])
+@Index('user_uuid_and_updated_at_timestamp_and_created_at_timestamp', [
+  'userUuid',
+  'updatedAtTimestamp',
+  'createdAtTimestamp',
+])
+@Index('user_uuid_and_deleted', ['userUuid', 'deleted'])
+export class TypeORMItem {
+  @PrimaryGeneratedColumn('uuid')
+  declare uuid: string
+
+  @Column({
+    type: 'varchar',
+    name: 'duplicate_of',
+    length: 36,
+    nullable: true,
+  })
+  declare duplicateOf: string | null
+
+  @Column({
+    type: 'varchar',
+    name: 'items_key_id',
+    length: 255,
+    nullable: true,
+  })
+  declare itemsKeyId: string | null
+
+  @Column({
+    type: 'text',
+    nullable: true,
+  })
+  declare content: string | null
+
+  @Column({
+    name: 'content_type',
+    type: 'varchar',
+    length: 255,
+    nullable: true,
+  })
+  @Index('index_items_on_content_type')
+  declare contentType: string | null
+
+  @Column({
+    name: 'content_size',
+    type: 'int',
+    nullable: true,
+  })
+  declare contentSize: number | null
+
+  @Column({
+    name: 'enc_item_key',
+    type: 'text',
+    nullable: true,
+  })
+  declare encItemKey: string | null
+
+  @Column({
+    name: 'auth_hash',
+    type: 'varchar',
+    length: 255,
+    nullable: true,
+  })
+  declare authHash: string | null
+
+  @Column({
+    name: 'user_uuid',
+    length: 36,
+  })
+  @Index('index_items_on_user_uuid')
+  declare userUuid: string
+
+  @Column({
+    type: 'tinyint',
+    precision: 1,
+    nullable: true,
+    default: 0,
+  })
+  @Index('index_items_on_deleted')
+  declare deleted: boolean
+
+  @Column({
+    name: 'created_at',
+    type: 'datetime',
+    precision: 6,
+  })
+  declare createdAt: Date
+
+  @Column({
+    name: 'updated_at',
+    type: 'datetime',
+    precision: 6,
+  })
+  declare updatedAt: Date
+
+  @Column({
+    name: 'created_at_timestamp',
+    type: 'bigint',
+  })
+  declare createdAtTimestamp: number
+
+  @Column({
+    name: 'updated_at_timestamp',
+    type: 'bigint',
+  })
+  @Index('updated_at_timestamp')
+  declare updatedAtTimestamp: number
+
+  @Column({
+    name: 'updated_with_session',
+    type: 'varchar',
+    length: 36,
+    nullable: true,
+  })
+  declare updatedWithSession: string | null
+}

+ 29 - 10
packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts

@@ -1,19 +1,24 @@
+import { ReadStream } from 'fs'
 import { Repository, SelectQueryBuilder } from 'typeorm'
 import { Repository, SelectQueryBuilder } from 'typeorm'
+import { MapperInterface } from '@standardnotes/domain-core'
+
 import { Item } from '../../Domain/Item/Item'
 import { Item } from '../../Domain/Item/Item'
 import { ItemQuery } from '../../Domain/Item/ItemQuery'
 import { ItemQuery } from '../../Domain/Item/ItemQuery'
 import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
 import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
-import { ReadStream } from 'fs'
 import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
 import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
+import { TypeORMItem } from './TypeORMItem'
 
 
 export class TypeORMItemRepository implements ItemRepositoryInterface {
 export class TypeORMItemRepository implements ItemRepositoryInterface {
-  constructor(private ormRepository: Repository<Item>) {}
+  constructor(private ormRepository: Repository<TypeORMItem>, private mapper: MapperInterface<Item, TypeORMItem>) {}
 
 
-  async save(item: Item): Promise<Item> {
-    return this.ormRepository.save(item)
+  async save(item: Item): Promise<void> {
+    const persistence = this.mapper.toProjection(item)
+
+    await this.ormRepository.save(persistence)
   }
   }
 
 
-  async remove(item: Item): Promise<Item> {
-    return this.ormRepository.remove(item)
+  async remove(item: Item): Promise<void> {
+    await this.ormRepository.remove(this.mapper.toProjection(item))
   }
   }
 
 
   async updateContentSize(itemUuid: string, contentSize: number): Promise<void> {
   async updateContentSize(itemUuid: string, contentSize: number): Promise<void> {
@@ -51,12 +56,18 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
   }
   }
 
 
   async findByUuid(uuid: string): Promise<Item | null> {
   async findByUuid(uuid: string): Promise<Item | null> {
-    return this.ormRepository
+    const persistence = await this.ormRepository
       .createQueryBuilder('item')
       .createQueryBuilder('item')
       .where('item.uuid = :uuid', {
       .where('item.uuid = :uuid', {
         uuid,
         uuid,
       })
       })
       .getOne()
       .getOne()
+
+    if (persistence === null) {
+      return null
+    }
+
+    return this.mapper.toDomain(persistence)
   }
   }
 
 
   async findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>> {
   async findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>> {
@@ -84,17 +95,25 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
   }
   }
 
 
   async findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null> {
   async findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null> {
-    return this.ormRepository
+    const persistence = await this.ormRepository
       .createQueryBuilder('item')
       .createQueryBuilder('item')
       .where('item.uuid = :uuid AND item.user_uuid = :userUuid', {
       .where('item.uuid = :uuid AND item.user_uuid = :userUuid', {
         uuid,
         uuid,
         userUuid,
         userUuid,
       })
       })
       .getOne()
       .getOne()
+
+    if (persistence === null) {
+      return null
+    }
+
+    return this.mapper.toDomain(persistence)
   }
   }
 
 
   async findAll(query: ItemQuery): Promise<Item[]> {
   async findAll(query: ItemQuery): Promise<Item[]> {
-    return this.createFindAllQueryBuilder(query).getMany()
+    const persistence = await this.createFindAllQueryBuilder(query).getMany()
+
+    return persistence.map((p) => this.mapper.toDomain(p))
   }
   }
 
 
   async findAllRaw<T>(query: ItemQuery): Promise<T[]> {
   async findAllRaw<T>(query: ItemQuery): Promise<T[]> {
@@ -126,7 +145,7 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
       .execute()
       .execute()
   }
   }
 
 
-  private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<Item> {
+  private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<TypeORMItem> {
     const queryBuilder = this.ormRepository.createQueryBuilder('item')
     const queryBuilder = this.ormRepository.createQueryBuilder('item')
 
 
     if (query.sortBy !== undefined && query.sortOrder !== undefined) {
     if (query.sortBy !== undefined && query.sortOrder !== undefined) {

+ 32 - 0
packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts

@@ -0,0 +1,32 @@
+import { MapperInterface } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+
+import { Item } from '../../Domain/Item/Item'
+import { ItemBackupRepresentation } from './ItemBackupRepresentation'
+
+export class ItemBackupMapper implements MapperInterface<Item, ItemBackupRepresentation> {
+  constructor(private timer: TimerInterface) {}
+
+  toDomain(_projection: ItemBackupRepresentation): Item {
+    throw new Error('Mapping from http representation to domain is not implemented.')
+  }
+
+  toProjection(domain: Item): ItemBackupRepresentation {
+    return {
+      uuid: domain.id.toString(),
+      items_key_id: domain.props.itemsKeyId,
+      duplicate_of: domain.props.duplicateOf ? domain.props.duplicateOf.value : null,
+      enc_item_key: domain.props.encItemKey,
+      content: domain.props.content,
+      content_type: domain.props.contentType.value as string,
+      auth_hash: domain.props.authHash,
+      deleted: !!domain.props.deleted,
+      created_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.createdAt),
+      created_at_timestamp: domain.props.timestamps.createdAt,
+      updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
+      updated_at_timestamp: domain.props.timestamps.updatedAt,
+      updated_with_session: domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null,
+      user_uuid: domain.props.userUuid.value,
+    }
+  }
+}

+ 16 - 0
packages/syncing-server/src/Mapping/Backup/ItemBackupRepresentation.ts

@@ -0,0 +1,16 @@
+export interface ItemBackupRepresentation {
+  uuid: string
+  items_key_id: string | null
+  duplicate_of: string | null
+  enc_item_key: string | null
+  content: string | null
+  content_type: string
+  auth_hash: string | null
+  deleted: boolean
+  created_at: string
+  created_at_timestamp: number
+  updated_at: string
+  updated_at_timestamp: number
+  updated_with_session: string | null
+  user_uuid: string
+}

+ 27 - 0
packages/syncing-server/src/Mapping/Http/ItemConflictHttpMapper.ts

@@ -0,0 +1,27 @@
+import { MapperInterface } from '@standardnotes/domain-core'
+
+import { Item } from '../../Domain/Item/Item'
+import { ItemConflictHttpRepresentation } from './ItemConflictHttpRepresentation'
+import { ItemConflict } from '../../Domain/Item/ItemConflict'
+import { ItemHttpRepresentation } from './ItemHttpRepresentation'
+
+export class ItemConflictHttpMapper implements MapperInterface<ItemConflict, ItemConflictHttpRepresentation> {
+  constructor(private mapper: MapperInterface<Item, ItemHttpRepresentation>) {}
+
+  toDomain(_projection: ItemConflictHttpRepresentation): ItemConflict {
+    throw new Error('Mapping from http representation to domain is not implemented.')
+  }
+
+  toProjection(domain: ItemConflict): ItemConflictHttpRepresentation {
+    const representation: ItemConflictHttpRepresentation = {
+      unsaved_item: domain.unsavedItem,
+      type: domain.type,
+    }
+
+    if (domain.serverItem) {
+      representation.server_item = this.mapper.toProjection(domain.serverItem)
+    }
+
+    return representation
+  }
+}

+ 10 - 0
packages/syncing-server/src/Mapping/Http/ItemConflictHttpRepresentation.ts

@@ -0,0 +1,10 @@
+import { ConflictType } from '@standardnotes/responses'
+
+import { ItemHash } from '../../Domain/Item/ItemHash'
+import { ItemHttpRepresentation } from './ItemHttpRepresentation'
+
+export interface ItemConflictHttpRepresentation {
+  server_item?: ItemHttpRepresentation
+  unsaved_item?: ItemHash
+  type: ConflictType
+}

+ 31 - 0
packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts

@@ -0,0 +1,31 @@
+import { MapperInterface } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+
+import { Item } from '../../Domain/Item/Item'
+import { ItemHttpRepresentation } from './ItemHttpRepresentation'
+
+export class ItemHttpMapper implements MapperInterface<Item, ItemHttpRepresentation> {
+  constructor(private timer: TimerInterface) {}
+
+  toDomain(_projection: ItemHttpRepresentation): Item {
+    throw new Error('Mapping from http representation to domain is not implemented.')
+  }
+
+  toProjection(domain: Item): ItemHttpRepresentation {
+    return {
+      uuid: domain.id.toString(),
+      items_key_id: domain.props.itemsKeyId,
+      duplicate_of: domain.props.duplicateOf ? domain.props.duplicateOf.value : null,
+      enc_item_key: domain.props.encItemKey,
+      content: domain.props.content,
+      content_type: domain.props.contentType.value as string,
+      auth_hash: domain.props.authHash,
+      deleted: !!domain.props.deleted,
+      created_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.createdAt),
+      created_at_timestamp: domain.props.timestamps.createdAt,
+      updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
+      updated_at_timestamp: domain.props.timestamps.updatedAt,
+      updated_with_session: domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null,
+    }
+  }
+}

+ 1 - 1
packages/syncing-server/src/Projection/ItemProjection.ts → packages/syncing-server/src/Mapping/Http/ItemHttpRepresentation.ts

@@ -1,4 +1,4 @@
-export type ItemProjection = {
+export interface ItemHttpRepresentation {
   uuid: string
   uuid: string
   items_key_id: string | null
   items_key_id: string | null
   duplicate_of: string | null
   duplicate_of: string | null

+ 27 - 0
packages/syncing-server/src/Mapping/Http/SavedItemHttpMapper.ts

@@ -0,0 +1,27 @@
+import { MapperInterface } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+
+import { Item } from '../../Domain/Item/Item'
+import { SavedItemHttpRepresentation } from './SavedItemHttpRepresentation'
+
+export class SavedItemHttpMapper implements MapperInterface<Item, SavedItemHttpRepresentation> {
+  constructor(private timer: TimerInterface) {}
+
+  toDomain(_projection: SavedItemHttpRepresentation): Item {
+    throw new Error('Mapping from http representation to domain is not implemented.')
+  }
+
+  toProjection(domain: Item): SavedItemHttpRepresentation {
+    return {
+      uuid: domain.id.toString(),
+      duplicate_of: domain.props.duplicateOf ? domain.props.duplicateOf.value : null,
+      content_type: domain.props.contentType.value as string,
+      auth_hash: domain.props.authHash,
+      deleted: !!domain.props.deleted,
+      created_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.createdAt),
+      created_at_timestamp: domain.props.timestamps.createdAt,
+      updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
+      updated_at_timestamp: domain.props.timestamps.updatedAt,
+    }
+  }
+}

+ 1 - 1
packages/syncing-server/src/Projection/SavedItemProjection.ts → packages/syncing-server/src/Mapping/Http/SavedItemHttpRepresentation.ts

@@ -1,4 +1,4 @@
-export type SavedItemProjection = {
+export interface SavedItemHttpRepresentation {
   uuid: string
   uuid: string
   duplicate_of: string | null
   duplicate_of: string | null
   content_type: string
   content_type: string

+ 96 - 0
packages/syncing-server/src/Mapping/Persistence/ItemPersistenceMapper.ts

@@ -0,0 +1,96 @@
+import { Timestamps, MapperInterface, UniqueEntityId, Uuid, ContentType, Dates } from '@standardnotes/domain-core'
+
+import { Item } from '../../Domain/Item/Item'
+
+import { TypeORMItem } from '../../Infra/TypeORM/TypeORMItem'
+
+export class ItemPersistenceMapper implements MapperInterface<Item, TypeORMItem> {
+  toDomain(projection: TypeORMItem): Item {
+    let duplicateOf = null
+    if (projection.duplicateOf) {
+      const duplicateOfOrError = Uuid.create(projection.duplicateOf)
+      if (duplicateOfOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${duplicateOfOrError.getError()}`)
+      }
+      duplicateOf = duplicateOfOrError.getValue()
+    }
+
+    const contentTypeOrError = ContentType.create(projection.contentType)
+    if (contentTypeOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${contentTypeOrError.getError()}`)
+    }
+    const contentType = contentTypeOrError.getValue()
+
+    const userUuidOrError = Uuid.create(projection.userUuid)
+    if (userUuidOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${userUuidOrError.getError()}`)
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const datesOrError = Dates.create(projection.createdAt, projection.updatedAt)
+    if (datesOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${datesOrError.getError()}`)
+    }
+    const dates = datesOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
+    if (timestampsOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${timestampsOrError.getError()}`)
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    let updatedWithSession = null
+    if (projection.updatedWithSession) {
+      const updatedWithSessionOrError = Uuid.create(projection.updatedWithSession)
+      if (updatedWithSessionOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${updatedWithSessionOrError.getError()}`)
+      }
+      updatedWithSession = updatedWithSessionOrError.getValue()
+    }
+
+    const itemOrError = Item.create(
+      {
+        duplicateOf,
+        itemsKeyId: projection.itemsKeyId,
+        content: projection.content,
+        contentType,
+        contentSize: projection.contentSize ?? undefined,
+        encItemKey: projection.encItemKey,
+        authHash: projection.authHash,
+        userUuid,
+        deleted: projection.deleted,
+        dates,
+        timestamps,
+        updatedWithSession,
+      },
+      new UniqueEntityId(projection.uuid),
+    )
+    if (itemOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${itemOrError.getError()}`)
+    }
+
+    return itemOrError.getValue()
+  }
+
+  toProjection(domain: Item): TypeORMItem {
+    const typeorm = new TypeORMItem()
+
+    typeorm.uuid = domain.id.toString()
+    typeorm.duplicateOf = domain.props.duplicateOf ? domain.props.duplicateOf.value : null
+    typeorm.itemsKeyId = domain.props.itemsKeyId
+    typeorm.content = domain.props.content
+    typeorm.contentType = domain.props.contentType.value
+    typeorm.contentSize = domain.props.contentSize ?? null
+    typeorm.encItemKey = domain.props.encItemKey
+    typeorm.authHash = domain.props.authHash
+    typeorm.userUuid = domain.props.userUuid.value
+    typeorm.deleted = domain.props.deleted
+    typeorm.createdAt = domain.props.dates.createdAt
+    typeorm.updatedAt = domain.props.dates.updatedAt
+    typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
+    typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
+    typeorm.updatedWithSession = domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null
+
+    return typeorm
+  }
+}

+ 0 - 10
packages/syncing-server/src/Projection/ItemConflictProjection.ts

@@ -1,10 +0,0 @@
-import { ConflictType } from '@standardnotes/responses'
-
-import { ItemHash } from '../Domain/Item/ItemHash'
-import { ItemProjection } from './ItemProjection'
-
-export type ItemConflictProjection = {
-  server_item?: ItemProjection
-  unsaved_item?: ItemHash
-  type: ConflictType
-}

+ 0 - 75
packages/syncing-server/src/Projection/ItemConflictProjector.spec.ts

@@ -1,75 +0,0 @@
-import 'reflect-metadata'
-
-import { ProjectorInterface } from './ProjectorInterface'
-import { Item } from '../Domain/Item/Item'
-import { ItemConflict } from '../Domain/Item/ItemConflict'
-import { ItemConflictProjector } from './ItemConflictProjector'
-import { ItemHash } from '../Domain/Item/ItemHash'
-import { ItemProjection } from './ItemProjection'
-import { ConflictType } from '@standardnotes/responses'
-
-describe('ItemConflictProjector', () => {
-  let itemProjector: ProjectorInterface<Item, ItemProjection>
-  let itemProjection: ItemProjection
-  let itemConflict1: ItemConflict
-  let itemConflict2: ItemConflict
-  let item: Item
-  let itemHash: ItemHash
-
-  const createProjector = () => new ItemConflictProjector(itemProjector)
-
-  beforeEach(() => {
-    itemProjection = {} as jest.Mocked<ItemProjection>
-
-    itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
-    itemProjector.projectFull = jest.fn().mockReturnValue(itemProjection)
-
-    item = {} as jest.Mocked<Item>
-
-    itemHash = {} as jest.Mocked<ItemHash>
-
-    itemConflict1 = {
-      serverItem: item,
-      type: ConflictType.ConflictingData,
-    }
-
-    itemConflict2 = {
-      unsavedItem: itemHash,
-      type: ConflictType.UuidConflict,
-    }
-  })
-
-  it('should create a full projection of a server item conflict', async () => {
-    expect(await createProjector().projectFull(itemConflict1)).toMatchObject({
-      server_item: itemProjection,
-      type: ConflictType.ConflictingData,
-    })
-  })
-
-  it('should create a full projection of an unsaved item conflict', async () => {
-    expect(await createProjector().projectFull(itemConflict2)).toMatchObject({
-      unsaved_item: itemHash,
-      type: 'uuid_conflict',
-    })
-  })
-
-  it('should throw error on custom projection', async () => {
-    let error = null
-    try {
-      await createProjector().projectCustom('test', itemConflict1)
-    } catch (e) {
-      error = e
-    }
-    expect((error as Error).message).toEqual('not implemented')
-  })
-
-  it('should throw error on simple projection', async () => {
-    let error = null
-    try {
-      await createProjector().projectSimple(itemConflict1)
-    } catch (e) {
-      error = e
-    }
-    expect((error as Error).message).toEqual('not implemented')
-  })
-})

+ 0 - 31
packages/syncing-server/src/Projection/ItemConflictProjector.ts

@@ -1,31 +0,0 @@
-import { ProjectorInterface } from './ProjectorInterface'
-
-import { Item } from '../Domain/Item/Item'
-import { ItemConflict } from '../Domain/Item/ItemConflict'
-import { ItemConflictProjection } from './ItemConflictProjection'
-import { ItemProjection } from './ItemProjection'
-
-export class ItemConflictProjector implements ProjectorInterface<ItemConflict, ItemConflictProjection> {
-  constructor(private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
-
-  async projectSimple(_itemConflict: ItemConflict): Promise<ItemConflictProjection> {
-    throw Error('not implemented')
-  }
-
-  async projectCustom(_projectionType: string, _itemConflict: ItemConflict): Promise<ItemConflictProjection> {
-    throw Error('not implemented')
-  }
-
-  async projectFull(itemConflict: ItemConflict): Promise<ItemConflictProjection> {
-    const projection: ItemConflictProjection = {
-      unsaved_item: itemConflict.unsavedItem,
-      type: itemConflict.type,
-    }
-
-    if (itemConflict.serverItem) {
-      projection.server_item = <ItemProjection>await this.itemProjector.projectFull(itemConflict.serverItem)
-    }
-
-    return projection
-  }
-}

+ 0 - 5
packages/syncing-server/src/Projection/ItemProjectionWithUser.ts

@@ -1,5 +0,0 @@
-import { ItemProjection } from './ItemProjection'
-
-export type ItemProjectionWithUser = ItemProjection & {
-  user_uuid: string
-}

+ 0 - 75
packages/syncing-server/src/Projection/ItemProjector.spec.ts

@@ -1,75 +0,0 @@
-import 'reflect-metadata'
-import { TimerInterface } from '@standardnotes/time'
-
-import { Item } from '../Domain/Item/Item'
-import { ItemProjector } from './ItemProjector'
-import { ContentType } from '@standardnotes/common'
-
-describe('ItemProjector', () => {
-  let item: Item
-  let timer: TimerInterface
-
-  const createProjector = () => new ItemProjector(timer)
-
-  beforeEach(() => {
-    timer = {} as jest.Mocked<TimerInterface>
-    timer.convertMicrosecondsToStringDate = jest.fn().mockReturnValue('2021-04-15T08:00:00.123456Z')
-
-    item = new Item()
-    item.uuid = '1-2-3'
-    item.itemsKeyId = '2-3-4'
-    item.duplicateOf = null
-    item.encItemKey = '3-4-5'
-    item.content = 'test'
-    item.contentType = ContentType.Note
-    item.authHash = 'asd'
-    item.deleted = false
-    item.createdAtTimestamp = 123
-    item.updatedAtTimestamp = 123
-    item.updatedWithSession = '7-6-5'
-    item.userUuid = 'u1-2-3'
-  })
-
-  it('should create a full projection of an item', async () => {
-    expect(await createProjector().projectFull(item)).toMatchObject({
-      uuid: '1-2-3',
-      items_key_id: '2-3-4',
-      duplicate_of: null,
-      enc_item_key: '3-4-5',
-      content: 'test',
-      content_type: 'Note',
-      auth_hash: 'asd',
-      deleted: false,
-      created_at: '2021-04-15T08:00:00.123456Z',
-      updated_at: '2021-04-15T08:00:00.123456Z',
-      updated_with_session: '7-6-5',
-    })
-  })
-
-  it('should create a custom projection of an item', async () => {
-    expect(await createProjector().projectCustom('dump', item)).toMatchObject({
-      uuid: '1-2-3',
-      items_key_id: '2-3-4',
-      duplicate_of: null,
-      enc_item_key: '3-4-5',
-      content: 'test',
-      content_type: 'Note',
-      auth_hash: 'asd',
-      deleted: false,
-      created_at: '2021-04-15T08:00:00.123456Z',
-      updated_at: '2021-04-15T08:00:00.123456Z',
-      updated_with_session: '7-6-5',
-      user_uuid: 'u1-2-3',
-    })
-  })
-
-  it('should throw error on simple projection', async () => {
-    let error = null
-    try {
-      await createProjector().projectSimple(item)
-    } catch (e) {
-      error = e
-    }
-    expect((error as Error).message).toEqual('not implemented')
-  })
-})

+ 0 - 41
packages/syncing-server/src/Projection/ItemProjector.ts

@@ -1,41 +0,0 @@
-import { TimerInterface } from '@standardnotes/time'
-import { ProjectorInterface } from './ProjectorInterface'
-
-import { Item } from '../Domain/Item/Item'
-import { ItemProjection } from './ItemProjection'
-import { ItemProjectionWithUser } from './ItemProjectionWithUser'
-
-export class ItemProjector implements ProjectorInterface<Item, ItemProjection> {
-  constructor(private timer: TimerInterface) {}
-
-  async projectSimple(_item: Item): Promise<ItemProjection> {
-    throw Error('not implemented')
-  }
-
-  async projectCustom(_projectionType: string, item: Item): Promise<ItemProjectionWithUser> {
-    const fullProjection = await this.projectFull(item)
-
-    return {
-      ...fullProjection,
-      user_uuid: item.userUuid,
-    }
-  }
-
-  async projectFull(item: Item): Promise<ItemProjection> {
-    return {
-      uuid: item.uuid,
-      items_key_id: item.itemsKeyId,
-      duplicate_of: item.duplicateOf,
-      enc_item_key: item.encItemKey,
-      content: item.content,
-      content_type: item.contentType as string,
-      auth_hash: item.authHash,
-      deleted: !!item.deleted,
-      created_at: this.timer.convertMicrosecondsToStringDate(item.createdAtTimestamp),
-      created_at_timestamp: item.createdAtTimestamp,
-      updated_at: this.timer.convertMicrosecondsToStringDate(item.updatedAtTimestamp),
-      updated_at_timestamp: item.updatedAtTimestamp,
-      updated_with_session: item.updatedWithSession,
-    }
-  }
-}

+ 0 - 5
packages/syncing-server/src/Projection/ProjectorInterface.ts

@@ -1,5 +0,0 @@
-export interface ProjectorInterface<T, E> {
-  projectSimple(object: T): Promise<Partial<E>>
-  projectFull(object: T): Promise<E>
-  projectCustom(projectionType: string, object: T, ...args: any[]): Promise<E>
-}

+ 0 - 64
packages/syncing-server/src/Projection/SavedItemProjector.spec.ts

@@ -1,64 +0,0 @@
-import 'reflect-metadata'
-import { TimerInterface } from '@standardnotes/time'
-
-import { Item } from '../Domain/Item/Item'
-import { SavedItemProjector } from './SavedItemProjector'
-import { ContentType } from '@standardnotes/common'
-
-describe('SavedItemProjector', () => {
-  let item: Item
-  let timer: TimerInterface
-
-  const createProjector = () => new SavedItemProjector(timer)
-
-  beforeEach(() => {
-    timer = {} as jest.Mocked<TimerInterface>
-    timer.convertMicrosecondsToStringDate = jest.fn().mockReturnValue('2021-04-15T08:00:00.123456Z')
-
-    item = new Item()
-    item.uuid = '1-2-3'
-    item.itemsKeyId = '2-3-4'
-    item.duplicateOf = null
-    item.encItemKey = '3-4-5'
-    item.content = 'test'
-    item.contentType = ContentType.Note
-    item.authHash = 'asd'
-    item.deleted = false
-    item.createdAtTimestamp = 123
-    item.updatedAtTimestamp = 123
-  })
-
-  it('should create a full projection of an item', async () => {
-    expect(await createProjector().projectFull(item)).toEqual({
-      uuid: '1-2-3',
-      duplicate_of: null,
-      content_type: 'Note',
-      auth_hash: 'asd',
-      deleted: false,
-      created_at: '2021-04-15T08:00:00.123456Z',
-      created_at_timestamp: 123,
-      updated_at: '2021-04-15T08:00:00.123456Z',
-      updated_at_timestamp: 123,
-    })
-  })
-
-  it('should throw error on custom projection', async () => {
-    let error = null
-    try {
-      await createProjector().projectCustom('test', item)
-    } catch (e) {
-      error = e
-    }
-    expect((error as Error).message).toEqual('not implemented')
-  })
-
-  it('should throw error on simple projection', async () => {
-    let error = null
-    try {
-      await createProjector().projectSimple(item)
-    } catch (e) {
-      error = e
-    }
-    expect((error as Error).message).toEqual('not implemented')
-  })
-})

部分文件因为文件数量过多而无法显示