diff --git a/.pnp.cjs b/.pnp.cjs index 4efd7bb70..235fb953c 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4560,17 +4560,16 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@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": [\ - ["@standardnotes/api", "npm:1.26.10"],\ + ["@standardnotes/api", "npm:1.26.25"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@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/utils", "npm:1.16.5"],\ + ["@standardnotes/utils", "npm:1.17.4"],\ ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ @@ -4635,7 +4634,7 @@ const RAW_RUNTIME_STATE = ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\ ["@simplewebauthn/server", "npm:7.2.0"],\ ["@simplewebauthn/typescript-types", "npm:7.0.0"],\ - ["@standardnotes/api", "npm:1.26.10"],\ + ["@standardnotes/api", "npm:1.26.25"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ @@ -4781,21 +4780,6 @@ const RAW_RUNTIME_STATE = "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", [\ ["workspace:packages/event-store", {\ "packageLocation": "./packages/event-store/",\ @@ -4841,6 +4825,17 @@ const RAW_RUNTIME_STATE = ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.59.6", {\ + "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip/node_modules/@standardnotes/features/",\ + "packageDependencies": [\ + ["@standardnotes/features", "npm:1.59.6"],\ + ["@standardnotes/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", [\ @@ -4935,14 +4930,15 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@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": [\ - ["@standardnotes/models", "npm:1.45.5"],\ + ["@standardnotes/models", "npm:1.46.7"],\ ["@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"]\ ],\ "linkType": "HARD"\ @@ -4977,6 +4973,17 @@ const RAW_RUNTIME_STATE = ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.13.26", {\ + "packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip/node_modules/@standardnotes/responses/",\ + "packageDependencies": [\ + ["@standardnotes/responses", "npm:1.13.26"],\ + ["@standardnotes/common", "workspace:packages/common"],\ + ["@standardnotes/features", "npm:1.59.6"],\ + ["@standardnotes/security", "workspace:packages/security"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@standardnotes/revisions-server", [\ @@ -4987,7 +4994,7 @@ const RAW_RUNTIME_STATE = ["@aws-sdk/client-s3", "npm:3.342.0"],\ ["@aws-sdk/client-sqs", "npm:3.342.0"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\ - ["@standardnotes/api", "npm:1.26.10"],\ + ["@standardnotes/api", "npm:1.26.25"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ @@ -5136,6 +5143,14 @@ const RAW_RUNTIME_STATE = ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.13.4", {\ + "packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip/node_modules/@standardnotes/sncrypto-common/",\ + "packageDependencies": [\ + ["@standardnotes/sncrypto-common", "npm:1.13.4"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@standardnotes/sncrypto-node", [\ @@ -5171,7 +5186,7 @@ const RAW_RUNTIME_STATE = ["@aws-sdk/client-sns", "npm:3.342.0"],\ ["@aws-sdk/client-sqs", "npm:3.342.0"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\ - ["@standardnotes/api", "npm:1.26.10"],\ + ["@standardnotes/api", "npm:1.26.25"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ @@ -5257,6 +5272,17 @@ const RAW_RUNTIME_STATE = ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.17.4", {\ + "packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip/node_modules/@standardnotes/utils/",\ + "packageDependencies": [\ + ["@standardnotes/utils", "npm:1.17.4"],\ + ["@standardnotes/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", [\ @@ -5266,7 +5292,7 @@ const RAW_RUNTIME_STATE = ["@standardnotes/websockets-server", "workspace:packages/websockets"],\ ["@aws-sdk/client-sqs", "npm:3.342.0"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.1"],\ - ["@standardnotes/api", "npm:1.26.10"],\ + ["@standardnotes/api", "npm:1.26.25"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ diff --git a/.yarn/cache/@standardnotes-api-npm-1.26.10-f6165cafd3-3c3561aec8.zip b/.yarn/cache/@standardnotes-api-npm-1.26.10-f6165cafd3-3c3561aec8.zip deleted file mode 100644 index 669c84dd5..000000000 Binary files a/.yarn/cache/@standardnotes-api-npm-1.26.10-f6165cafd3-3c3561aec8.zip and /dev/null differ diff --git a/.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip b/.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip new file mode 100644 index 000000000..addc1c5d0 Binary files /dev/null and b/.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip differ diff --git a/.yarn/cache/@standardnotes-encryption-npm-1.21.38-d08c3d4766-1393840523.zip b/.yarn/cache/@standardnotes-encryption-npm-1.21.38-d08c3d4766-1393840523.zip deleted file mode 100644 index b6d100e1f..000000000 Binary files a/.yarn/cache/@standardnotes-encryption-npm-1.21.38-d08c3d4766-1393840523.zip and /dev/null differ diff --git a/.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip b/.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip new file mode 100644 index 000000000..99ec807f9 Binary files /dev/null and b/.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip differ diff --git a/.yarn/cache/@standardnotes-models-npm-1.45.5-29326e959c-15f26c11b2.zip b/.yarn/cache/@standardnotes-models-npm-1.45.5-29326e959c-15f26c11b2.zip deleted file mode 100644 index 56d415a0b..000000000 Binary files a/.yarn/cache/@standardnotes-models-npm-1.45.5-29326e959c-15f26c11b2.zip and /dev/null differ diff --git a/.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip b/.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip new file mode 100644 index 000000000..ac12c5aa4 Binary files /dev/null and b/.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip differ diff --git a/.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip b/.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip new file mode 100644 index 000000000..5140b6fc4 Binary files /dev/null and b/.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip differ diff --git a/.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip b/.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip new file mode 100644 index 000000000..6749a2e48 Binary files /dev/null and b/.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip differ diff --git a/.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip b/.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip new file mode 100644 index 000000000..f925d7202 Binary files /dev/null and b/.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip differ diff --git a/packages/auth/package.json b/packages/auth/package.json index c6328941f..f908bccac 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -42,7 +42,7 @@ "@cbor-extract/cbor-extract-linux-x64": "^2.1.1", "@simplewebauthn/server": "^7.2.0", "@simplewebauthn/typescript-types": "^7.0.0", - "@standardnotes/api": "^1.25.3", + "@standardnotes/api": "^1.26.25", "@standardnotes/common": "workspace:*", "@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-events": "workspace:*", diff --git a/packages/auth/src/Controller/AuthController.spec.ts b/packages/auth/src/Controller/AuthController.spec.ts index a790afbc6..a8faf2f88 100644 --- a/packages/auth/src/Controller/AuthController.spec.ts +++ b/packages/auth/src/Controller/AuthController.spec.ts @@ -8,12 +8,12 @@ import { User } from '../Domain/User/User' import { Register } from '../Domain/UseCase/Register' import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface' import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' -import { ApiVersion } from '@standardnotes/api' import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes' import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery' import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes' import { Logger } from 'winston' import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface' +import { ApiVersion } from '../Domain/Api/ApiVersion' describe('AuthController', () => { let clearLoginAttempts: ClearLoginAttempts @@ -73,7 +73,7 @@ describe('AuthController', () => { email: 'test@test.te', password: 'asdzxc', version: ProtocolVersion.V004, - api: ApiVersion.v0, + api: ApiVersion.v20200115, origination: KeyParamsOrigination.Registration, userAgent: 'Google Chrome', identifier: 'test@test.te', @@ -103,7 +103,7 @@ describe('AuthController', () => { email: 'test@test.te', password: '', version: ProtocolVersion.V004, - api: ApiVersion.v0, + api: ApiVersion.v20200115, origination: KeyParamsOrigination.Registration, userAgent: 'Google Chrome', identifier: 'test@test.te', @@ -123,7 +123,7 @@ describe('AuthController', () => { email: 'test@test.te', password: 'test', version: ProtocolVersion.V004, - api: ApiVersion.v0, + api: ApiVersion.v20200115, origination: KeyParamsOrigination.Registration, userAgent: 'Google Chrome', identifier: 'test@test.te', diff --git a/packages/auth/src/Controller/AuthController.ts b/packages/auth/src/Controller/AuthController.ts index 17ecb6c8b..7611fe3d9 100644 --- a/packages/auth/src/Controller/AuthController.ts +++ b/packages/auth/src/Controller/AuthController.ts @@ -1,10 +1,10 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { - ApiVersion, UserRegistrationRequestParams, UserServerInterface, UserDeletionResponseBody, UserRegistrationResponseBody, + UserUpdateRequestParams, } from '@standardnotes/api' import { ErrorTag, HttpResponse, HttpStatusCode } from '@standardnotes/responses' import { ProtocolVersion } from '@standardnotes/common' @@ -23,6 +23,8 @@ import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/G import { GenerateRecoveryCodesRequestParams } from '../Infra/Http/Request/GenerateRecoveryCodesRequestParams' import { Logger } from 'winston' 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 { constructor( @@ -37,6 +39,10 @@ export class AuthController implements UserServerInterface { private sessionService: SessionServiceInterface, ) {} + async update(_params: UserUpdateRequestParams): Promise> { + throw new Error('Method not implemented.') + } + async deleteAccount(_params: never): Promise> { throw new Error('This method is implemented on the payments server.') } @@ -121,7 +127,7 @@ export class AuthController implements UserServerInterface { async signInWithRecoveryCodes( params: SignInWithRecoveryCodesRequestParams, ): Promise> { - if (params.apiVersion !== ApiVersion.v0) { + if (params.apiVersion !== ApiVersion.v20200115) { return { status: HttpStatusCode.BadRequest, data: { @@ -162,7 +168,7 @@ export class AuthController implements UserServerInterface { async recoveryKeyParams( params: RecoveryKeyParamsRequestParams, ): Promise> { - if (params.apiVersion !== ApiVersion.v0) { + if (params.apiVersion !== ApiVersion.v20200115) { return { status: HttpStatusCode.BadRequest, data: { diff --git a/packages/auth/src/Controller/SubscriptionInvitesController.spec.ts b/packages/auth/src/Controller/SubscriptionInvitesController.spec.ts index 57d2c4f0d..247507cfb 100644 --- a/packages/auth/src/Controller/SubscriptionInvitesController.spec.ts +++ b/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 { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation' import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations' -import { ApiVersion } from '@standardnotes/api' +import { ApiVersion } from '../Domain/Api/ApiVersion' describe('SubscriptionInvitesController', () => { let inviteToSharedSubscription: InviteToSharedSubscription @@ -53,7 +53,7 @@ describe('SubscriptionInvitesController', () => { 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({ inviterEmail: 'test@test.te', @@ -68,7 +68,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().cancelInvite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, inviteUuid: '1-2-3', inviterEmail: 'test@test.te', }) @@ -87,7 +87,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().cancelInvite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, inviteUuid: '1-2-3', }) @@ -100,7 +100,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().declineInvite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, inviteUuid: '1-2-3', }) @@ -117,7 +117,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().declineInvite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, inviteUuid: '1-2-3', }) @@ -134,7 +134,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().acceptInvite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, inviteUuid: '1-2-3', }) @@ -151,7 +151,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().acceptInvite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, inviteUuid: '1-2-3', }) @@ -168,7 +168,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().invite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, identifier: 'invitee@test.te', inviterUuid: '1-2-3', 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 () => { const result = await createController().invite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, identifier: '', inviterUuid: '1-2-3', inviterEmail: 'test@test.te', @@ -205,7 +205,7 @@ describe('SubscriptionInvitesController', () => { }) const result = await createController().invite({ - api: ApiVersion.v0, + api: ApiVersion.v20200115, identifier: 'invitee@test.te', inviterUuid: '1-2-3', inviterEmail: 'test@test.te', diff --git a/packages/auth/src/Domain/UseCase/Register.ts b/packages/auth/src/Domain/UseCase/Register.ts index 0ae91512f..b66e9f940 100644 --- a/packages/auth/src/Domain/UseCase/Register.ts +++ b/packages/auth/src/Domain/UseCase/Register.ts @@ -1,6 +1,5 @@ import * as bcrypt from 'bcryptjs' import { RoleName, Username } from '@standardnotes/domain-core' -import { ApiVersion } from '@standardnotes/api' import { v4 as uuidv4 } from 'uuid' import { inject, injectable } from 'inversify' @@ -16,6 +15,7 @@ import { TimerInterface } from '@standardnotes/time' import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115' import { AuthResponse20200115 } from '../Auth/AuthResponse20200115' +import { ApiVersion } from '../Api/ApiVersion' @injectable() export class Register implements UseCaseInterface { @@ -39,7 +39,7 @@ export class Register implements UseCaseInterface { const { email, password, apiVersion, ephemeralSession, ...registrationFields } = dto - if (apiVersion !== ApiVersion.v0) { + if (apiVersion !== ApiVersion.v20200115) { return { success: false, errorMessage: `Unsupported api version: ${apiVersion}`, diff --git a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts index 84b3065da..82bb4c80a 100644 --- a/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts +++ b/packages/auth/src/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes.ts @@ -1,7 +1,6 @@ import * as bcrypt from 'bcryptjs' import { Result, UseCaseInterface, Username, Uuid, Validator } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/settings' -import { ApiVersion } from '@standardnotes/api' import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115' import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' @@ -16,6 +15,7 @@ import { IncreaseLoginAttempts } from '../IncreaseLoginAttempts' import { ClearLoginAttempts } from '../ClearLoginAttempts' import { DeleteSetting } from '../DeleteSetting/DeleteSetting' import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' +import { ApiVersion } from '../../Api/ApiVersion' export class SignInWithRecoveryCodes implements UseCaseInterface { constructor( @@ -100,7 +100,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface> = { - [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] -} diff --git a/packages/common/src/Domain/index.ts b/packages/common/src/Domain/index.ts index bff742891..bbca01b96 100644 --- a/packages/common/src/Domain/index.ts +++ b/packages/common/src/Domain/index.ts @@ -1,4 +1,3 @@ -export * from './Content/ContentType' export * from './Content/ContentDecoder' export * from './Content/ContentDecoderInterface' export * from './DataType/AnyRecord' diff --git a/packages/domain-core/src/Domain/Common/ContentType.spec.ts b/packages/domain-core/src/Domain/Common/ContentType.spec.ts new file mode 100644 index 000000000..0a8189321 --- /dev/null +++ b/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') + }) +}) diff --git a/packages/domain-core/src/Domain/Common/ContentType.ts b/packages/domain-core/src/Domain/Common/ContentType.ts new file mode 100644 index 000000000..c23f68327 --- /dev/null +++ b/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 { + 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> = { + [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 { + if (type === null) { + return Result.ok(new ContentType({ value: null })) + } + + const isValidType = Object.values(this.TYPES).includes(type) + if (!isValidType) { + return Result.fail(`Invalid content type: ${type}`) + } else { + return Result.ok(new ContentType({ value: type })) + } + } + + getDisplayName(): string | null { + if (!this.value) { + return null + } + + return this.displayNamesMap[this.value] || this.value + } +} diff --git a/packages/revisions/src/Domain/Revision/ContentTypeProps.ts b/packages/domain-core/src/Domain/Common/ContentTypeProps.ts similarity index 100% rename from packages/revisions/src/Domain/Revision/ContentTypeProps.ts rename to packages/domain-core/src/Domain/Common/ContentTypeProps.ts diff --git a/packages/domain-core/src/Domain/index.ts b/packages/domain-core/src/Domain/index.ts index 1737838a7..bc0feca99 100644 --- a/packages/domain-core/src/Domain/index.ts +++ b/packages/domain-core/src/Domain/index.ts @@ -9,6 +9,8 @@ export * from './Cache/CacheEntry' export * from './Cache/CacheEntryProps' export * from './Cache/CacheEntryRepositoryInterface' +export * from './Common/ContentType' +export * from './Common/ContentTypeProps' export * from './Common/Dates' export * from './Common/DatesProps' export * from './Common/Email' diff --git a/packages/revisions/package.json b/packages/revisions/package.json index 34a8cca92..b3963b043 100644 --- a/packages/revisions/package.json +++ b/packages/revisions/package.json @@ -27,7 +27,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.332.0", "@aws-sdk/client-sqs": "^3.332.0", - "@standardnotes/api": "^1.25.3", + "@standardnotes/api": "^1.26.25", "@standardnotes/common": "workspace:^", "@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-events": "workspace:*", diff --git a/packages/revisions/src/Domain/Revision/ContentType.spec.ts b/packages/revisions/src/Domain/Revision/ContentType.spec.ts deleted file mode 100644 index 12a538cc9..000000000 --- a/packages/revisions/src/Domain/Revision/ContentType.spec.ts +++ /dev/null @@ -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() - }) -}) diff --git a/packages/revisions/src/Domain/Revision/ContentType.ts b/packages/revisions/src/Domain/Revision/ContentType.ts deleted file mode 100644 index 75b4b63a2..000000000 --- a/packages/revisions/src/Domain/Revision/ContentType.ts +++ /dev/null @@ -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 { - get value(): string | null { - return this.props.value - } - - private constructor(props: ContentTypeProps) { - super(props) - } - - static create(contentType: string | null): Result { - if (contentType !== null && !Object.values(ContentTypeValues).includes(contentType as ContentTypeValues)) { - return Result.fail(`Value is not a valid content type: ${contentType}`) - } else { - return Result.ok(new ContentType({ value: contentType })) - } - } -} diff --git a/packages/revisions/src/Domain/Revision/Revision.spec.ts b/packages/revisions/src/Domain/Revision/Revision.spec.ts index 3787c8263..3c877a044 100644 --- a/packages/revisions/src/Domain/Revision/Revision.spec.ts +++ b/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' describe('Revision', () => { diff --git a/packages/revisions/src/Domain/Revision/RevisionMetadataProps.ts b/packages/revisions/src/Domain/Revision/RevisionMetadataProps.ts index 17a767426..3f5f670e6 100644 --- a/packages/revisions/src/Domain/Revision/RevisionMetadataProps.ts +++ b/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 { contentType: ContentType diff --git a/packages/revisions/src/Domain/Revision/RevisionProps.ts b/packages/revisions/src/Domain/Revision/RevisionProps.ts index b1a60b1b3..e3db9f713 100644 --- a/packages/revisions/src/Domain/Revision/RevisionProps.ts +++ b/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 { itemUuid: Uuid diff --git a/packages/revisions/src/Mapping/RevisionItemStringMapper.ts b/packages/revisions/src/Mapping/RevisionItemStringMapper.ts index 11def46e7..1a11bf617 100644 --- a/packages/revisions/src/Mapping/RevisionItemStringMapper.ts +++ b/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' export class RevisionItemStringMapper implements MapperInterface { diff --git a/packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts b/packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts index 76c8765fd..e92ea6588 100644 --- a/packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts +++ b/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 { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision' diff --git a/packages/revisions/src/Mapping/RevisionPersistenceMapper.ts b/packages/revisions/src/Mapping/RevisionPersistenceMapper.ts index b93bf01fd..c371f954e 100644 --- a/packages/revisions/src/Mapping/RevisionPersistenceMapper.ts +++ b/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 { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision' diff --git a/packages/syncing-server/package.json b/packages/syncing-server/package.json index 1e98a5989..f06c7e6d8 100644 --- a/packages/syncing-server/package.json +++ b/packages/syncing-server/package.json @@ -31,7 +31,7 @@ "@aws-sdk/client-s3": "^3.332.0", "@aws-sdk/client-sns": "^3.332.0", "@aws-sdk/client-sqs": "^3.332.0", - "@standardnotes/api": "^1.25.3", + "@standardnotes/api": "^1.26.25", "@standardnotes/common": "workspace:*", "@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-events": "workspace:*", diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 642d125ab..37fe149a5 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -9,9 +9,6 @@ import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface' import { TypeORMItemRepository } from '../Infra/TypeORM/TypeORMItemRepository' import { Repository } from 'typeorm' import { Item } from '../Domain/Item/Item' -import { ItemProjection } from '../Projection/ItemProjection' -import { ProjectorInterface } from '../Projection/ProjectorInterface' -import { ItemProjector } from '../Projection/ItemProjector' import { DirectCallDomainEventPublisher, DirectCallEventMessageHandler, @@ -26,15 +23,12 @@ import { Timer, TimerInterface } from '@standardnotes/time' import { ItemTransferCalculatorInterface } from '../Domain/Item/ItemTransferCalculatorInterface' import { ItemTransferCalculator } from '../Domain/Item/ItemTransferCalculator' import { ItemConflict } from '../Domain/Item/ItemConflict' -import { ItemFactory } from '../Domain/Item/ItemFactory' -import { ItemFactoryInterface } from '../Domain/Item/ItemFactoryInterface' import { ItemService } from '../Domain/Item/ItemService' import { ItemServiceInterface } from '../Domain/Item/ItemServiceInterface' import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter' import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter' import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter' import { TimeDifferenceFilter } from '../Domain/Item/SaveRule/TimeDifferenceFilter' -import { UuidFilter } from '../Domain/Item/SaveRule/UuidFilter' import { ItemSaveValidator } from '../Domain/Item/SaveValidator/ItemSaveValidator' import { ItemSaveValidatorInterface } from '../Domain/Item/SaveValidator/ItemSaveValidatorInterface' 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 { SyncItems } from '../Domain/UseCase/Syncing/SyncItems/SyncItems' 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 { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs' import { ContentDecoder } from '@standardnotes/common' @@ -70,9 +60,21 @@ import { ItemBackupServiceInterface } from '../Domain/Item/ItemBackupServiceInte import { FSItemBackupService } from '../Infra/FS/FSItemBackupService' import { AuthHttpService } from '../Infra/HTTP/AuthHttpService' 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 { 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 { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -122,6 +124,8 @@ export class ContainerConfigLoader { logger.debug('Database initialized') + container.bind(TYPES.Sync_Timer).toConstantValue(new Timer()) + const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server' container.bind(TYPES.Sync_Env).toConstantValue(env) @@ -201,24 +205,37 @@ export class ContainerConfigLoader { }) } - // Repositories - container.bind(TYPES.Sync_ItemRepository).toDynamicValue((context: interfaces.Context) => { - return new TypeORMItemRepository(context.container.get(TYPES.Sync_ORMItemRepository)) - }) + // Mapping + container + .bind>(TYPES.Sync_ItemPersistenceMapper) + .toConstantValue(new ItemPersistenceMapper()) + container + .bind>(TYPES.Sync_ItemHttpMapper) + .toConstantValue(new ItemHttpMapper(container.get(TYPES.Sync_Timer))) + container + .bind>(TYPES.Sync_SavedItemHttpMapper) + .toConstantValue(new SavedItemHttpMapper(container.get(TYPES.Sync_Timer))) + container + .bind>(TYPES.Sync_ItemConflictHttpMapper) + .toConstantValue(new ItemConflictHttpMapper(container.get(TYPES.Sync_ItemHttpMapper))) + container + .bind>(TYPES.Sync_ItemBackupMapper) + .toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer))) // ORM container - .bind>(TYPES.Sync_ORMItemRepository) - .toDynamicValue(() => appDataSource.getRepository(Item)) + .bind>(TYPES.Sync_ORMItemRepository) + .toDynamicValue(() => appDataSource.getRepository(TypeORMItem)) - // Projectors + // Repositories container - .bind>(TYPES.Sync_ItemProjector) - .toDynamicValue((context: interfaces.Context) => { - return new ItemProjector(context.container.get(TYPES.Sync_Timer)) - }) - - container.bind(TYPES.Sync_Timer).toDynamicValue(() => new Timer()) + .bind(TYPES.Sync_ItemRepository) + .toConstantValue( + new TypeORMItemRepository( + container.get(TYPES.Sync_ORMItemRepository), + container.get(TYPES.Sync_ItemPersistenceMapper), + ), + ) container .bind(TYPES.Sync_DomainEventFactory) @@ -245,18 +262,6 @@ export class ContainerConfigLoader { ) }) - // Projectors - container - .bind>(TYPES.Sync_SavedItemProjector) - .toDynamicValue((context: interfaces.Context) => { - return new SavedItemProjector(context.container.get(TYPES.Sync_Timer)) - }) - container - .bind>(TYPES.Sync_ItemConflictProjector) - .toDynamicValue((context: interfaces.Context) => { - return new ItemConflictProjector(context.container.get(TYPES.Sync_ItemProjector)) - }) - // env vars container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET')) container @@ -287,60 +292,35 @@ export class ContainerConfigLoader { container.bind(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => { return new GetItem(context.container.get(TYPES.Sync_ItemRepository)) }) + container + .bind(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 + .bind(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), + ), + ) // Services - container.bind(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 - .bind(TYPES.Sync_SyncResponseFactory20161215) - .toDynamicValue((context: interfaces.Context) => { - return new SyncResponseFactory20161215(context.container.get(TYPES.Sync_ItemProjector)) - }) - container - .bind(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), - ) - }) - container - .bind(TYPES.Sync_SyncResponseFactoryResolver) - .toDynamicValue((context: interfaces.Context) => { - return new SyncResponseFactoryResolver( - context.container.get(TYPES.Sync_SyncResponseFactory20161215), - context.container.get(TYPES.Sync_SyncResponseFactory20200115), - ) - }) - - container.bind(TYPES.Sync_ItemFactory).toDynamicValue((context: interfaces.Context) => { - return new ItemFactory(context.container.get(TYPES.Sync_Timer), context.container.get(TYPES.Sync_ItemProjector)) - }) - - container.bind(TYPES.Sync_OwnershipFilter).toDynamicValue(() => new OwnershipFilter()) + container.bind(TYPES.Sync_OwnershipFilter).toConstantValue(new OwnershipFilter()) container .bind(TYPES.Sync_TimeDifferenceFilter) - .toDynamicValue( - (context: interfaces.Context) => new TimeDifferenceFilter(context.container.get(TYPES.Sync_Timer)), - ) - container.bind(TYPES.Sync_UuidFilter).toDynamicValue(() => new UuidFilter()) - container.bind(TYPES.Sync_ContentTypeFilter).toDynamicValue(() => new ContentTypeFilter()) - container.bind(TYPES.Sync_ContentFilter).toDynamicValue(() => new ContentFilter()) + .toConstantValue(new TimeDifferenceFilter(container.get(TYPES.Sync_Timer))) + container.bind(TYPES.Sync_ContentTypeFilter).toConstantValue(new ContentTypeFilter()) + container.bind(TYPES.Sync_ContentFilter).toConstantValue(new ContentFilter()) container .bind(TYPES.Sync_ItemSaveValidator) @@ -348,12 +328,47 @@ export class ContainerConfigLoader { return new ItemSaveValidator([ context.container.get(TYPES.Sync_OwnershipFilter), context.container.get(TYPES.Sync_TimeDifferenceFilter), - context.container.get(TYPES.Sync_UuidFilter), context.container.get(TYPES.Sync_ContentTypeFilter), context.container.get(TYPES.Sync_ContentFilter), ]) }) + container + .bind(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(TYPES.Sync_SyncResponseFactory20161215) + .toConstantValue(new SyncResponseFactory20161215(container.get(TYPES.Sync_ItemHttpMapper))) + container + .bind(TYPES.Sync_SyncResponseFactory20200115) + .toConstantValue( + new SyncResponseFactory20200115( + container.get(TYPES.Sync_ItemHttpMapper), + container.get(TYPES.Sync_ItemConflictHttpMapper), + container.get(TYPES.Sync_SavedItemHttpMapper), + ), + ) + container + .bind(TYPES.Sync_SyncResponseFactoryResolver) + .toDynamicValue((context: interfaces.Context) => { + return new SyncResponseFactoryResolver( + context.container.get(TYPES.Sync_SyncResponseFactory20161215), + context.container.get(TYPES.Sync_SyncResponseFactory20200115), + ) + }) + // env vars container .bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE) @@ -421,14 +436,15 @@ export class ContainerConfigLoader { if (env.get('S3_AWS_REGION', true)) { return new S3ItemBackupService( 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_S3), ) } else { return new FSItemBackupService( 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), ) } @@ -512,7 +528,7 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_SyncItems), container.get(TYPES.Sync_CheckIntegrity), container.get(TYPES.Sync_GetItem), - container.get(TYPES.Sync_ItemProjector), + container.get(TYPES.Sync_ItemHttpMapper), container.get(TYPES.Sync_SyncResponseFactoryResolver), container.get(TYPES.Sync_ControllerContainer), ), diff --git a/packages/syncing-server/src/Bootstrap/DataSource.ts b/packages/syncing-server/src/Bootstrap/DataSource.ts index 755d5bfdc..acaf07252 100644 --- a/packages/syncing-server/src/Bootstrap/DataSource.ts +++ b/packages/syncing-server/src/Bootstrap/DataSource.ts @@ -1,9 +1,9 @@ import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } from 'typeorm' import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions' -import { Item } from '../Domain/Item/Item' -import { Notification } from '../Domain/Notifications/Notification' import { Env } from './Env' import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions' +import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem' +import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification' export class AppDataSource { private _dataSource: DataSource | undefined @@ -33,7 +33,7 @@ export class AppDataSource { const commonDataSourceOptions = { maxQueryExecutionTime, - entities: [Item, Notification], + entities: [TypeORMItem, TypeORMNotification], migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`], migrationsRun: true, logging: this.env.get('DB_DEBUG_LEVEL', true) ?? 'info', diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index e78f595d9..f10777821 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -12,10 +12,6 @@ const TYPES = { Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'), // Middleware 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 Sync_REDIS_URL: Symbol.for('Sync_REDIS_URL'), Sync_SNS_TOPIC_ARN: Symbol.for('Sync_SNS_TOPIC_ARN'), @@ -57,6 +53,8 @@ const TYPES = { Sync_SendMessageToUser: Symbol.for('Sync_SendMessageToUser'), Sync_DeleteAllMessagesSentToUser: Symbol.for('Sync_DeleteAllMessagesSentToUser'), Sync_DeleteMessage: Symbol.for('Sync_DeleteMessage'), + Sync_SaveNewItem: Symbol.for('Sync_SaveNewItem'), + Sync_UpdateExistingItem: Symbol.for('Sync_UpdateExistingItem'), // Handlers Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'), Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'), @@ -80,10 +78,8 @@ const TYPES = { Sync_ItemSaveValidator: Symbol.for('Sync_ItemSaveValidator'), Sync_OwnershipFilter: Symbol.for('Sync_OwnershipFilter'), Sync_TimeDifferenceFilter: Symbol.for('Sync_TimeDifferenceFilter'), - Sync_UuidFilter: Symbol.for('Sync_UuidFilter'), Sync_ContentTypeFilter: Symbol.for('Sync_ContentTypeFilter'), Sync_ContentFilter: Symbol.for('Sync_ContentFilter'), - Sync_ItemFactory: Symbol.for('Sync_ItemFactory'), Sync_ItemTransferCalculator: Symbol.for('Sync_ItemTransferCalculator'), Sync_ControllerContainer: Symbol.for('Sync_ControllerContainer'), Sync_HomeServerItemsController: Symbol.for('Sync_HomeServerItemsController'), @@ -92,6 +88,11 @@ const TYPES = { Sync_SharedVaultUserHttpMapper: Symbol.for('Sync_SharedVaultUserHttpMapper'), Sync_SharedVaultInviteHttpMapper: Symbol.for('Sync_SharedVaultInviteHttpMapper'), 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 diff --git a/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.spec.ts b/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.spec.ts index 4e7ef8312..db62d04e5 100644 --- a/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.spec.ts +++ b/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.spec.ts @@ -9,6 +9,7 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface' import { ExtensionsHttpService } from './ExtensionsHttpService' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { AxiosInstance } from 'axios' +import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core' describe('ExtensionsHttpService', () => { let httpClient: AxiosInstance @@ -34,9 +35,22 @@ describe('ExtensionsHttpService', () => { httpClient = {} as jest.Mocked httpClient.request = jest.fn().mockReturnValue({ status: 200, data: { foo: 'bar' } }) - item = { - content: 'test', - } as jest.Mocked + 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 diff --git a/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.ts b/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.ts index a2d04cfa6..2bb5d106e 100644 --- a/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.ts +++ b/packages/syncing-server/src/Domain/Extension/ExtensionsHttpService.ts @@ -140,11 +140,11 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface { email: string, ): Promise { 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}`) } - const content = this.contentDecoder.decode(extension.content) + const content = this.contentDecoder.decode(extension.props.content) switch (this.getExtensionName(content)) { case ExtensionName.Dropbox: return this.createCloudBackupFailedEventBasedOnProvider('DROPBOX', email) diff --git a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts b/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts index 5f402aa59..4dbec93bb 100644 --- a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts +++ b/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts @@ -5,6 +5,7 @@ import { Logger } from 'winston' import { Item } from '../Item/Item' import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface' import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler' +import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core' describe('AccountDeletionRequestedEventHandler', () => { let itemRepository: ItemRepositoryInterface @@ -15,10 +16,22 @@ describe('AccountDeletionRequestedEventHandler', () => { const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepository, logger) beforeEach(() => { - item = { - uuid: '1-2-3', - content: 'test', - } as jest.Mocked + 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 itemRepository.findAll = jest.fn().mockReturnValue([item]) diff --git a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts b/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts index cc369da1d..880757863 100644 --- a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts +++ b/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts @@ -10,6 +10,7 @@ import { Item } from '../Item/Item' import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface' import { DuplicateItemSyncedEventHandler } from './DuplicateItemSyncedEventHandler' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' +import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core' describe('DuplicateItemSyncedEventHandler', () => { let itemRepository: ItemRepositoryInterface @@ -24,14 +25,39 @@ describe('DuplicateItemSyncedEventHandler', () => { new DuplicateItemSyncedEventHandler(itemRepository, domainEventFactory, domainEventPublisher, logger) beforeEach(() => { - originalItem = { - uuid: '1-2-3', - } as jest.Mocked + 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 = { - uuid: '2-3-4', - duplicateOf: '1-2-3', - } as jest.Mocked + 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 itemRepository.findByUuidAndUserUuid = jest @@ -81,7 +107,7 @@ describe('DuplicateItemSyncedEventHandler', () => { }) 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) expect(domainEventPublisher.publish).not.toHaveBeenCalled() diff --git a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts index a0faac7f0..0f8e367bb 100644 --- a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts +++ b/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts @@ -24,22 +24,22 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf return } - if (!item.duplicateOf) { + if (!item.props.duplicateOf) { this.logger.warn(`Item ${event.payload.itemUuid} does not point to any duplicate`) return } const existingOriginalItem = await this.itemRepository.findByUuidAndUserUuid( - item.duplicateOf, + item.props.duplicateOf.value, event.payload.userUuid, ) if (existingOriginalItem !== null) { await this.domainEventPublisher.publish( this.domainEventFactory.createRevisionsCopyRequestedEvent(event.payload.userUuid, { - originalItemUuid: existingOriginalItem.uuid, - newItemUuid: item.uuid, + originalItemUuid: existingOriginalItem.id.toString(), + newItemUuid: item.id.toString(), }), ) } diff --git a/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts b/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts index 73e9111ac..7e9103c61 100644 --- a/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts +++ b/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts @@ -11,6 +11,7 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface' import { ItemRevisionCreationRequestedEventHandler } from './ItemRevisionCreationRequestedEventHandler' import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' +import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core' describe('ItemRevisionCreationRequestedEventHandler', () => { let itemRepository: ItemRepositoryInterface @@ -29,10 +30,22 @@ describe('ItemRevisionCreationRequestedEventHandler', () => { ) beforeEach(() => { - item = { - uuid: '1-2-3', - content: 'test', - } as jest.Mocked + 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 itemRepository.findByUuid = jest.fn().mockReturnValue(item) diff --git a/packages/syncing-server/src/Domain/Item/ExtendedIntegrityPayload.ts b/packages/syncing-server/src/Domain/Item/ExtendedIntegrityPayload.ts index ebf13eb38..bd0b62f53 100644 --- a/packages/syncing-server/src/Domain/Item/ExtendedIntegrityPayload.ts +++ b/packages/syncing-server/src/Domain/Item/ExtendedIntegrityPayload.ts @@ -1,6 +1,5 @@ -import { ContentType } from '@standardnotes/common' import { IntegrityPayload } from '@standardnotes/responses' export type ExtendedIntegrityPayload = IntegrityPayload & { - content_type: ContentType + content_type: string | null } diff --git a/packages/syncing-server/src/Domain/Item/Item.ts b/packages/syncing-server/src/Domain/Item/Item.ts index 27b62b54e..2e12198ad 100644 --- a/packages/syncing-server/src/Domain/Item/Item.ts +++ b/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 { + 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 { + 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(new Item(props, id)) + } } diff --git a/packages/syncing-server/src/Domain/Item/ItemFactory.spec.ts b/packages/syncing-server/src/Domain/Item/ItemFactory.spec.ts deleted file mode 100644 index 4c987fa94..000000000 --- a/packages/syncing-server/src/Domain/Item/ItemFactory.spec.ts +++ /dev/null @@ -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 - let timeHelper: Timer - - const createFactory = () => new ItemFactory(timer, itemProjector) - - beforeEach(() => { - timeHelper = new Timer() - - timer = {} as jest.Mocked - 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> - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - 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', - }) - }) -}) diff --git a/packages/syncing-server/src/Domain/Item/ItemFactory.ts b/packages/syncing-server/src/Domain/Item/ItemFactory.ts deleted file mode 100644 index 19733ec6e..000000000 --- a/packages/syncing-server/src/Domain/Item/ItemFactory.ts +++ /dev/null @@ -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) {} - - 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 - } -} diff --git a/packages/syncing-server/src/Domain/Item/ItemFactoryInterface.ts b/packages/syncing-server/src/Domain/Item/ItemFactoryInterface.ts deleted file mode 100644 index e94c1bb13..000000000 --- a/packages/syncing-server/src/Domain/Item/ItemFactoryInterface.ts +++ /dev/null @@ -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 -} diff --git a/packages/syncing-server/src/Domain/Item/ItemHash.ts b/packages/syncing-server/src/Domain/Item/ItemHash.ts index 29ffaa65b..42b1e780e 100644 --- a/packages/syncing-server/src/Domain/Item/ItemHash.ts +++ b/packages/syncing-server/src/Domain/Item/ItemHash.ts @@ -1,9 +1,7 @@ -import { ContentType } from '@standardnotes/common' - export type ItemHash = { uuid: string content?: string - content_type: ContentType + content_type: string | null deleted?: boolean duplicate_of?: string | null auth_hash?: string diff --git a/packages/syncing-server/src/Domain/Item/ItemProps.ts b/packages/syncing-server/src/Domain/Item/ItemProps.ts new file mode 100644 index 000000000..1601f23ca --- /dev/null +++ b/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 +} diff --git a/packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts b/packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts index cabee487b..af3faeade 100644 --- a/packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts @@ -16,8 +16,8 @@ export interface ItemRepositoryInterface { findItemsForComputingIntegrityPayloads(userUuid: string): Promise findByUuidAndUserUuid(uuid: string, userUuid: string): Promise findByUuid(uuid: string): Promise - remove(item: Item): Promise - save(item: Item): Promise + remove(item: Item): Promise + save(item: Item): Promise markItemsAsDeleted(itemUuids: Array, updatedAtTimestamp: number): Promise updateContentSize(itemUuid: string, contentSize: number): Promise } diff --git a/packages/syncing-server/src/Domain/Item/ItemService.spec.ts b/packages/syncing-server/src/Domain/Item/ItemService.spec.ts index b03d7e4e7..db1441e21 100644 --- a/packages/syncing-server/src/Domain/Item/ItemService.spec.ts +++ b/packages/syncing-server/src/Domain/Item/ItemService.spec.ts @@ -1,94 +1,101 @@ import 'reflect-metadata' -import { ContentType } from '@standardnotes/common' +import { Timer, TimerInterface } from '@standardnotes/time' +import { Logger } from 'winston' + import { Item } from './Item' import { ItemHash } from './ItemHash' import { ItemRepositoryInterface } from './ItemRepositoryInterface' import { ItemService } from './ItemService' import { ApiVersion } from '../Api/ApiVersion' -import { 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 { ItemFactoryInterface } from './ItemFactoryInterface' import { ItemConflict } from './ItemConflict' 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', () => { let itemRepository: ItemRepositoryInterface - let domainEventPublisher: DomainEventPublisherInterface - let domainEventFactory: DomainEventFactoryInterface - const revisionFrequency = 300 const contentSizeTransferLimit = 100 let timer: TimerInterface let item1: Item let item2: Item let itemHash1: ItemHash let itemHash2: ItemHash - let emptyHash: ItemHash let syncToken: string let logger: Logger let itemSaveValidator: ItemSaveValidatorInterface let newItem: Item - let itemFactory: ItemFactoryInterface let timeHelper: Timer let itemTransferCalculator: ItemTransferCalculatorInterface - let itemProjector: ProjectorInterface + let saveNewItemUseCase: SaveNewItem + let updateExistingItemUseCase: UpdateExistingItem const maxItemsSyncLimit = 300 const createService = () => new ItemService( itemSaveValidator, - itemFactory, itemRepository, - domainEventPublisher, - domainEventFactory, - revisionFrequency, contentSizeTransferLimit, itemTransferCalculator, timer, - itemProjector, maxItemsSyncLimit, + saveNewItemUseCase, + updateExistingItemUseCase, logger, ) beforeEach(() => { 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 - item2 = { - uuid: '2-3-4', - userUuid: '1-2-3', - createdAt: new Date(1616164633241312), - createdAtTimestamp: 1616164633241312, - updatedAt: new Date(1616164633241312), - updatedAtTimestamp: 1616164633241312, - } as jest.Mocked + item1 = Item.create( + { + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + updatedWithSession: null, + content: 'foobar1', + contentType: ContentType.create(ContentType.TYPES.Note).getValue(), + encItemKey: null, + authHash: null, + itemsKeyId: null, + duplicateOf: null, + deleted: false, + dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(), + timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(), + }, + new UniqueEntityId('00000000-0000-0000-0000-000000000000'), + ).getValue() + item2 = Item.create( + { + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + updatedWithSession: null, + content: 'foobar2', + contentType: ContentType.create(ContentType.TYPES.Note).getValue(), + encItemKey: null, + authHash: null, + itemsKeyId: null, + duplicateOf: null, + deleted: false, + dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(), + timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(), + }, + new UniqueEntityId('00000000-0000-0000-0000-000000000001'), + ).getValue() itemHash1 = { uuid: '1-2-3', content: 'asdqwe1', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, duplicate_of: null, enc_item_key: 'qweqwe1', items_key_id: 'asdasd1', created_at: timeHelper.formatDate( - timeHelper.convertMicrosecondsToDate(item1.createdAtTimestamp), + timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', ), 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]', ), } as jest.Mocked @@ -96,31 +103,28 @@ describe('ItemService', () => { itemHash2 = { uuid: '2-3-4', content: 'asdqwe2', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, duplicate_of: null, enc_item_key: 'qweqwe2', items_key_id: 'asdasd2', created_at: timeHelper.formatDate( - timeHelper.convertMicrosecondsToDate(item2.createdAtTimestamp), + timeHelper.convertMicrosecondsToDate(item2.props.timestamps.createdAt), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', ), 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]', ), } as jest.Mocked - emptyHash = { - uuid: '2-3-4', - } as jest.Mocked - itemTransferCalculator = {} as jest.Mocked - itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockReturnValue([item1.uuid, item2.uuid]) + itemTransferCalculator.computeItemUuidsToFetch = jest + .fn() + .mockReturnValue([item1.id.toString(), item2.id.toString()]) itemRepository = {} as jest.Mocked itemRepository.findAll = jest.fn().mockReturnValue([item1, item2]) itemRepository.countAll = jest.fn().mockReturnValue(2) - itemRepository.save = jest.fn().mockImplementation((item: Item) => item) timer = {} as jest.Mocked timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568) @@ -136,13 +140,6 @@ describe('ItemService', () => { .fn() .mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds)) - domainEventPublisher = {} as jest.Mocked - domainEventPublisher.publish = jest.fn() - - domainEventFactory = {} as jest.Mocked - domainEventFactory.createDuplicateItemSyncedEvent = jest.fn() - domainEventFactory.createItemRevisionCreationRequested = jest.fn() - logger = {} as jest.Mocked logger.error = jest.fn() logger.warn = jest.fn() @@ -152,31 +149,28 @@ describe('ItemService', () => { itemSaveValidator = {} as jest.Mocked itemSaveValidator.validate = jest.fn().mockReturnValue({ passed: true }) - newItem = { - contentType: ContentType.Note, - } as jest.Mocked + 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() - itemFactory = {} as jest.Mocked - itemFactory.create = jest.fn().mockReturnValue(newItem) - itemFactory.createStub = jest.fn().mockReturnValue(newItem) + saveNewItemUseCase = {} as jest.Mocked + saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(newItem)) - itemProjector = {} as jest.Mocked> - 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', - }) + updateExistingItemUseCase = {} as jest.Mocked + 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 () => { @@ -186,7 +180,7 @@ describe('ItemService', () => { await createService().getItems({ userUuid: '1-2-3', syncToken, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }), ).toEqual({ items: [item1, item2], @@ -202,7 +196,7 @@ describe('ItemService', () => { limit: 150, }) expect(itemRepository.findAll).toHaveBeenCalledWith({ - uuids: ['1-2-3', '2-3-4'], + uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'], sortOrder: 'ASC', sortBy: 'updated_at_timestamp', }) @@ -213,7 +207,7 @@ describe('ItemService', () => { await createService().getItems({ userUuid: '1-2-3', syncToken, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }), ).toEqual({ items: [item1, item2], @@ -229,7 +223,7 @@ describe('ItemService', () => { limit: 150, }) 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', sortOrder: 'ASC', }) @@ -240,7 +234,7 @@ describe('ItemService', () => { await createService().getItems({ userUuid: '1-2-3', syncToken, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, limit: 1000, }), ).toEqual({ @@ -257,7 +251,7 @@ describe('ItemService', () => { limit: 300, }) 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', sortOrder: 'ASC', }) @@ -270,7 +264,7 @@ describe('ItemService', () => { await createService().getItems({ userUuid: '1-2-3', syncToken, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }), ).toEqual({ items: [], @@ -295,7 +289,7 @@ describe('ItemService', () => { userUuid: '1-2-3', syncToken, limit: 1, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }) expect(itemsResponse).toEqual({ @@ -315,7 +309,7 @@ describe('ItemService', () => { limit: 1, }) 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', sortOrder: 'ASC', }) @@ -329,7 +323,7 @@ describe('ItemService', () => { userUuid: '1-2-3', syncToken, cursorToken, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }), ).toEqual({ items: [item1, item2], @@ -345,7 +339,7 @@ describe('ItemService', () => { limit: 150, }) 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', sortOrder: 'ASC', }) @@ -355,7 +349,7 @@ describe('ItemService', () => { expect( await createService().getItems({ userUuid: '1-2-3', - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }), ).toEqual({ items: [item1, item2], @@ -371,7 +365,7 @@ describe('ItemService', () => { limit: 150, }) 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', sortOrder: 'ASC', }) @@ -381,7 +375,7 @@ describe('ItemService', () => { await createService().getItems({ userUuid: '1-2-3', syncToken, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }) expect(itemRepository.countAll).toHaveBeenCalledWith({ @@ -394,7 +388,7 @@ describe('ItemService', () => { limit: 150, }) expect(itemRepository.findAll).toHaveBeenCalledWith({ - uuids: ['1-2-3', '2-3-4'], + uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'], sortOrder: 'ASC', sortBy: 'updated_at_timestamp', }) @@ -405,7 +399,7 @@ describe('ItemService', () => { userUuid: '1-2-3', syncToken, limit: 0, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }) expect(itemRepository.countAll).toHaveBeenCalledWith({ @@ -418,7 +412,7 @@ describe('ItemService', () => { limit: 150, }) 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', sortOrder: 'ASC', }) @@ -432,7 +426,7 @@ describe('ItemService', () => { userUuid: '1-2-3', syncToken: '2:', limit: 0, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }) } catch (e) { error = e @@ -449,7 +443,7 @@ describe('ItemService', () => { userUuid: '1-2-3', syncToken: '1234567890', limit: 0, - contentType: ContentType.Note, + contentType: ContentType.TYPES.Note, }) } catch (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 () => { - const item3 = { - uuid: '1-2-3', - } as jest.Mocked - const item4 = { - uuid: '4-5-6', - } as jest.Mocked + 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]) @@ -485,11 +505,10 @@ describe('ItemService', () => { expect(result).toEqual({ conflicts: [], 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 () => { @@ -513,16 +532,29 @@ describe('ItemService', () => { savedItems: [], syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==', }) + + expect(saveNewItemUseCase.execute).not.toHaveBeenCalled() }) it('should save new items that are duplicates', async () => { itemRepository.findByUuid = jest.fn().mockReturnValue(null) - const duplicateItem = { - updatedAtTimestamp: 1616164633241570, - duplicateOf: '1-2-3', - contentType: ContentType.Note, - } as jest.Mocked - 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({ itemHashes: [itemHash1], @@ -537,10 +569,6 @@ describe('ItemService', () => { savedItems: [duplicateItem], 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 () => { @@ -568,7 +596,7 @@ describe('ItemService', () => { it('should mark items as saved that are skipped on validation', async () => { itemRepository.findByUuid = jest.fn().mockReturnValue(null) - const skipped = {} as jest.Mocked + const skipped = item1 const validationResult = { passed: false, skipped } itemSaveValidator.validate = jest.fn().mockReturnValue(validationResult) @@ -583,7 +611,7 @@ describe('ItemService', () => { expect(result).toEqual({ conflicts: [], savedItems: [skipped], - syncToken: 'MjpOYU4=', + syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMg==', }) }) @@ -593,7 +621,7 @@ describe('ItemService', () => { const itemHash3 = { uuid: '3-4-5', content: 'asdqwe3', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, duplicate_of: null, enc_item_key: 'qweqwe3', items_key_id: 'asdasd3', @@ -607,11 +635,41 @@ describe('ItemService', () => { const item3Timestamp = 1616164633241569 timer.getTimestampInMicroseconds = jest.fn().mockReturnValueOnce(saveProcedureStartTimestamp) - itemFactory.create = jest + saveNewItemUseCase.execute = jest .fn() - .mockReturnValueOnce({ updatedAtTimestamp: item1Timestamp, duplicateOf: null } as jest.Mocked) - .mockReturnValueOnce({ updatedAtTimestamp: item2Timestamp, duplicateOf: null } as jest.Mocked) - .mockReturnValueOnce({ updatedAtTimestamp: item3Timestamp, duplicateOf: null } as jest.Mocked) + .mockReturnValueOnce( + Result.ok( + Item.create( + { + ...item1.props, + timestamps: Timestamps.create(item1Timestamp, item1Timestamp).getValue(), + }, + new UniqueEntityId('00000000-0000-0000-0000-000000000001'), + ).getValue(), + ), + ) + .mockReturnValueOnce( + Result.ok( + Item.create( + { + ...item2.props, + timestamps: Timestamps.create(item2Timestamp, item2Timestamp).getValue(), + }, + new UniqueEntityId('00000000-0000-0000-0000-000000000002'), + ).getValue(), + ), + ) + .mockReturnValueOnce( + Result.ok( + Item.create( + { + ...item2.props, + timestamps: Timestamps.create(item3Timestamp, item3Timestamp).getValue(), + }, + new UniqueEntityId('00000000-0000-0000-0000-000000000003'), + ).getValue(), + ), + ) const result = await createService().saveItems({ itemHashes: [itemHash1, itemHash3, itemHash2], @@ -638,66 +696,14 @@ describe('ItemService', () => { 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 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==', - }) - }) - - 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) + updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops')) const result = await createService().saveItems({ itemHashes: [itemHash1], @@ -708,240 +714,49 @@ describe('ItemService', () => { }) expect(result).toEqual({ - conflicts: [], - savedItems: [ + conflicts: [ { - 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', + type: 'uuid_conflict', + unsavedItem: itemHash1, }, ], + savedItems: [], 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: [ - { - 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', - }, - ], - syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==', - }) - }) - - it('should mark existing item as deleted', async () => { - itemRepository.findByUuid = jest.fn().mockReturnValue(item1) - - itemHash1.deleted = true - 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: 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', - }, - ], - 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', - }, - ], - 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 fails', async () => { itemRepository.findByUuid = jest.fn().mockReturnValue(null) - itemRepository.save = jest.fn().mockImplementation(() => { - throw new Error('Something bad happened') + saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops')) + + const result = await createService().saveItems({ + itemHashes: [itemHash1, itemHash2], + userUuid: '1-2-3', + apiVersion: ApiVersion.v20200115, + readOnlyAccess: false, + sessionUuid: '2-3-4', + }) + + expect(result).toEqual({ + conflicts: [ + { + type: 'uuid_conflict', + unsavedItem: itemHash1, + }, + { + type: 'uuid_conflict', + unsavedItem: itemHash2, + }, + ], + savedItems: [], + syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==', + }) + }) + + it('should skip saving conflicting items and mark them as sync conflicts when saving throws an error', async () => { + itemRepository.findByUuid = jest.fn().mockReturnValue(null) + saveNewItemUseCase.execute = jest.fn().mockImplementation(() => { + throw new Error('Oops') }) const result = await createService().saveItems({ diff --git a/packages/syncing-server/src/Domain/Item/ItemService.ts b/packages/syncing-server/src/Domain/Item/ItemService.ts index 14cc40edf..2a02a8bae 100644 --- a/packages/syncing-server/src/Domain/Item/ItemService.ts +++ b/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 { ContentType } from '@standardnotes/common' import { Logger } from 'winston' -import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { GetItemsDTO } from './GetItemsDTO' import { GetItemsResult } from './GetItemsResult' import { Item } from './Item' import { ItemConflict } from './ItemConflict' -import { ItemFactoryInterface } from './ItemFactoryInterface' -import { ItemHash } from './ItemHash' import { ItemQuery } from './ItemQuery' import { ItemRepositoryInterface } from './ItemRepositoryInterface' import { ItemServiceInterface } from './ItemServiceInterface' @@ -18,8 +13,9 @@ import { SaveItemsResult } from './SaveItemsResult' import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface' import { ConflictType } from '@standardnotes/responses' 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 { private readonly DEFAULT_ITEMS_LIMIT = 150 @@ -27,16 +23,13 @@ export class ItemService implements ItemServiceInterface { constructor( private itemSaveValidator: ItemSaveValidatorInterface, - private itemFactory: ItemFactoryInterface, private itemRepository: ItemRepositoryInterface, - private domainEventPublisher: DomainEventPublisherInterface, - private domainEventFactory: DomainEventFactoryInterface, - private revisionFrequency: number, private contentSizeTransferLimit: number, private itemTransferCalculator: ItemTransferCalculatorInterface, private timer: TimerInterface, - private itemProjector: ProjectorInterface, private maxItemsSyncLimit: number, + private saveNewItem: SaveNewItem, + private updateExistingItem: UpdateExistingItem, private logger: Logger, ) {} @@ -73,7 +66,7 @@ export class ItemService implements ItemServiceInterface { let cursorToken = undefined 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') } @@ -118,15 +111,47 @@ export class ItemService implements ItemServiceInterface { } if (existingItem) { - const updatedItem = await this.updateExistingItem({ + const udpatedItemOrError = await this.updateExistingItem.execute({ existingItem, itemHash, sessionUuid: dto.sessionUuid, }) + if (udpatedItemOrError.isFailed()) { + this.logger.error( + `[${dto.userUuid}] Updating item ${itemHash.uuid} failed. Error: ${udpatedItemOrError.getError()}`, + ) + + conflicts.push({ + unsavedItem: itemHash, + type: ConflictType.UuidConflict, + }) + + continue + } + const updatedItem = udpatedItemOrError.getValue() + savedItems.push(updatedItem) } else { try { - const 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) } catch (error) { 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): Promise> { const itemsKeys = await this.itemRepository.findAll({ userUuid, - contentType: ContentType.ItemsKey, + contentType: ContentType.TYPES.ItemsKey, sortBy: 'updated_at_timestamp', sortOrder: 'ASC', }) - const retrievedItemsIds: Array = retrievedItems.map((item: Item) => item.uuid) + const retrievedItemsIds: Array = retrievedItems.map((item: Item) => item.id.toString()) itemsKeys.forEach((itemKey: Item) => { - if (retrievedItemsIds.indexOf(itemKey.uuid) === -1) { + if (retrievedItemsIds.indexOf(itemKey.id.toString()) === -1) { retrievedItems.unshift(itemKey) } }) @@ -172,9 +197,9 @@ export class ItemService implements ItemServiceInterface { private calculateSyncToken(lastUpdatedTimestamp: number, savedItems: Array): string { if (savedItems.length) { 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 @@ -187,103 +212,6 @@ export class ItemService implements ItemServiceInterface { ).toString('base64') } - private async updateExistingItem(dto: { - existingItem: Item - itemHash: ItemHash - sessionUuid: string | null - }): Promise { - 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 { - 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 { let token = dto.syncToken if (dto.cursorToken !== undefined && dto.cursorToken !== null) { diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.spec.ts index 3eb14a85b..b969be759 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.spec.ts @@ -1,11 +1,10 @@ import 'reflect-metadata' -import { ContentType } from '@standardnotes/common' - import { ApiVersion } from '../../Api/ApiVersion' import { Item } from '../Item' import { ContentFilter } from './ContentFilter' +import { ContentType } from '@standardnotes/domain-core' describe('ContentFilter', () => { let existingItem: Item @@ -21,7 +20,7 @@ describe('ContentFilter', () => { itemHash: { uuid: '123e4567-e89b-12d3-a456-426655440000', content: invalidContent as unknown as string, - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, existingItem: null, }) @@ -32,7 +31,7 @@ describe('ContentFilter', () => { unsavedItem: { uuid: '123e4567-e89b-12d3-a456-426655440000', content: invalidContent, - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, type: 'content_error', }, @@ -50,7 +49,7 @@ describe('ContentFilter', () => { itemHash: { uuid: '123e4567-e89b-12d3-a456-426655440000', content: validContent as unknown as string, - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, existingItem, }) diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.spec.ts index ab3638899..b43e56a63 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.spec.ts @@ -1,5 +1,5 @@ -import { ContentType } from '@standardnotes/common' import 'reflect-metadata' + import { ApiVersion } from '../../Api/ApiVersion' import { Item } from '../Item' @@ -27,7 +27,7 @@ describe('ContentTypeFilter', () => { apiVersion: ApiVersion.v20200115, itemHash: { uuid: '123e4567-e89b-12d3-a456-426655440000', - content_type: invalidContentType as ContentType, + content_type: invalidContentType, }, existingItem: null, }) @@ -54,7 +54,7 @@ describe('ContentTypeFilter', () => { apiVersion: ApiVersion.v20200115, itemHash: { uuid: '123e4567-e89b-12d3-a456-426655440000', - content_type: validContentType as ContentType, + content_type: validContentType, }, existingItem, }) diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts index 7620e27e1..843fa034c 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.ts +++ b/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 { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' -import { ConflictType } from '@standardnotes/responses' export class ContentTypeFilter implements ItemSaveRuleInterface { async check(dto: ItemSaveValidationDTO): Promise { - 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 { passed: false, conflict: { diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.spec.ts index bffba2396..2f2eaa8fe 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.spec.ts @@ -1,28 +1,41 @@ import 'reflect-metadata' -import { ContentType } from '@standardnotes/common' - import { ApiVersion } from '../../Api/ApiVersion' import { Item } from '../Item' import { OwnershipFilter } from './OwnershipFilter' +import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core' describe('OwnershipFilter', () => { let existingItem: Item const createFilter = () => new OwnershipFilter() beforeEach(() => { - existingItem = {} as jest.Mocked - 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 () => { const result = await createFilter().check({ - userUuid: '1-2-3', + userUuid: '00000000-0000-0000-0000-000000000001', apiVersion: ApiVersion.v20200115, itemHash: { uuid: '2-3-4', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, existingItem, }) @@ -32,7 +45,7 @@ describe('OwnershipFilter', () => { conflict: { unsavedItem: { uuid: '2-3-4', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, type: 'uuid_conflict', }, @@ -40,14 +53,12 @@ describe('OwnershipFilter', () => { }) it('should leave items belonging to the same user', async () => { - existingItem.userUuid = '1-2-3' - const result = await createFilter().check({ - userUuid: '1-2-3', + userUuid: '00000000-0000-0000-0000-000000000000', apiVersion: ApiVersion.v20200115, itemHash: { uuid: '2-3-4', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, existingItem, }) @@ -59,11 +70,11 @@ describe('OwnershipFilter', () => { it('should leave non existing items', async () => { const result = await createFilter().check({ - userUuid: '1-2-3', + userUuid: '00000000-0000-0000-0000-000000000000', apiVersion: ApiVersion.v20200115, itemHash: { uuid: '2-3-4', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, existingItem: null, }) @@ -72,4 +83,27 @@ describe('OwnershipFilter', () => { 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', + }, + }) + }) }) diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts index fc8e1d9fc..33703b225 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts @@ -2,10 +2,23 @@ import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO' import { ItemSaveRuleResult } from './ItemSaveRuleResult' import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' import { ConflictType } from '@standardnotes/responses' +import { Uuid } from '@standardnotes/domain-core' export class OwnershipFilter implements ItemSaveRuleInterface { async check(dto: ItemSaveValidationDTO): Promise { - 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) { return { passed: false, diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts index 8266b5abc..61a6fd67e 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts @@ -1,8 +1,7 @@ import 'reflect-metadata' -import { ContentType } from '@standardnotes/common' - import { Time, Timer, TimerInterface } from '@standardnotes/time' +import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core' import { ApiVersion } from '../../Api/ApiVersion' @@ -26,28 +25,36 @@ describe('TimeDifferenceFilter', () => { .fn() .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 + 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 = { uuid: '1-2-3', content: 'asdqwe1', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, duplicate_of: null, enc_item_key: 'qweqwe1', items_key_id: 'asdasd1', created_at: timeHelper.formatDate( - timeHelper.convertMicrosecondsToDate(existingItem.createdAtTimestamp), + timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.createdAt), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', ), updated_at: timeHelper.formatDate( - timeHelper.convertMicrosecondsToDate(existingItem.updatedAtTimestamp + 1), + timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt + 1), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', ), } as jest.Mocked @@ -83,7 +90,7 @@ describe('TimeDifferenceFilter', () => { }) 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({ userUuid: '1-2-3', @@ -102,7 +109,7 @@ describe('TimeDifferenceFilter', () => { }) 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({ 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 () => { itemHash.updated_at = timeHelper.formatDate( 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]', ) @@ -142,7 +151,7 @@ describe('TimeDifferenceFilter', () => { it('should leave items having update at timestamp different by less then a second for legacy clients', async () => { itemHash.updated_at = timeHelper.formatDate( - timeHelper.convertMicrosecondsToDate(existingItem.updatedAtTimestamp), + timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt), '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 () => { itemHash.updated_at = timeHelper.formatDate( new Date( - timeHelper.convertMicrosecondsToMilliseconds(existingItem.updatedAtTimestamp) + + timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) + Time.MicrosecondsInAMillisecond + 1, ), @@ -186,7 +195,7 @@ describe('TimeDifferenceFilter', () => { it('should leave items having update at timestamp different by less than a millisecond', async () => { itemHash.updated_at = timeHelper.formatDate( - timeHelper.convertMicrosecondsToDate(existingItem.updatedAtTimestamp), + timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', ) diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts index 3e039d790..7bcb7efde 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.ts +++ b/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 if (this.itemHashHasMicrosecondsPrecision(dto.itemHash)) { diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.spec.ts deleted file mode 100644 index d3a81d623..000000000 --- a/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.spec.ts +++ /dev/null @@ -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, - }) - - expect(result).toEqual({ - passed: true, - }) - } - }) -}) diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts deleted file mode 100644 index a42c2e84c..000000000 --- a/packages/syncing-server/src/Domain/Item/SaveRule/UuidFilter.ts +++ /dev/null @@ -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 { - const validUuid = validate(dto.itemHash.uuid) - - if (!validUuid) { - return { - passed: false, - conflict: { - unsavedItem: dto.itemHash, - type: ConflictType.UuidError, - }, - } - } - - return { - passed: true, - } - } -} diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts index dd716caa3..7b46ab5ee 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20161215.ts @@ -1,13 +1,13 @@ import { ConflictType } from '@standardnotes/responses' import { ItemHash } from '../ItemHash' -import { ItemProjection } from '../../../Projection/ItemProjection' +import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' export type SyncResponse20161215 = { - retrieved_items: Array - saved_items: Array + retrieved_items: Array + saved_items: Array unsaved: Array<{ - item: ItemProjection | ItemHash + item: ItemHttpRepresentation | ItemHash error: { tag: ConflictType } diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts index c53e4ae6f..007e63899 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts +++ b/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 = { - retrieved_items: Array - saved_items: Array - conflicts: Array + retrieved_items: Array + saved_items: Array + conflicts: Array sync_token: string cursor_token?: string } diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts index 2e0b3a1e3..0c191e461 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.spec.ts @@ -1,16 +1,16 @@ import 'reflect-metadata' -import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { Item } from '../Item' import { ItemHash } from '../ItemHash' -import { ItemProjection } from '../../../Projection/ItemProjection' import { SyncResponseFactory20161215 } from './SyncResponseFactory20161215' import { ConflictType } from '@standardnotes/responses' +import { ContentType, Dates, MapperInterface, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core' +import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' describe('SyncResponseFactory20161215', () => { - let itemProjector: ProjectorInterface - let item1Projection: ItemProjection - let item2Projection: ItemProjection + let itemProjector: MapperInterface + let item1Projection: ItemHttpRepresentation + let item2Projection: ItemHttpRepresentation let item1: Item let item2: Item @@ -19,30 +19,55 @@ describe('SyncResponseFactory20161215', () => { beforeEach(() => { item1Projection = { uuid: '1-2-3', - } as jest.Mocked + } as jest.Mocked item2Projection = { uuid: '2-3-4', - } as jest.Mocked + } as jest.Mocked - itemProjector = {} as jest.Mocked> - itemProjector.projectFull = jest.fn().mockImplementation((item: Item) => { - if (item.uuid === '1-2-3') { + itemProjector = {} as jest.Mocked> + itemProjector.toProjection = jest.fn().mockImplementation((item: Item) => { + if (item.id.toString() === '00000000-0000-0000-0000-000000000000') { return item1Projection - } else if (item.uuid === '2-3-4') { + } else if (item.id.toString() === '00000000-0000-0000-0000-000000000001') { return item2Projection } return undefined }) - item1 = { - uuid: '1-2-3', - updatedAtTimestamp: 100, - } as jest.Mocked + 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 + 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 () => { @@ -83,10 +108,18 @@ describe('SyncResponseFactory20161215', () => { it('should pick out conflicts between saved and retrieved items and remove them from the later', async () => { const itemHash1 = {} as jest.Mocked - 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( await createFactory().createResponse({ diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts index d26606289..0c9258ef6 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20161215.ts @@ -1,18 +1,18 @@ import { ConflictType } from '@standardnotes/responses' +import { MapperInterface } from '@standardnotes/domain-core' -import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { Item } from '../Item' import { ItemConflict } from '../ItemConflict' import { ItemHash } from '../ItemHash' -import { ItemProjection } from '../../../Projection/ItemProjection' import { SyncResponse20161215 } from './SyncResponse20161215' import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface' import { SyncItemsResponse } from '../../UseCase/Syncing/SyncItems/SyncItemsResponse' +import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface { private readonly LEGACY_MIN_CONFLICT_INTERVAL = 20_000_000 - constructor(private itemProjector: ProjectorInterface) {} + constructor(private mapper: MapperInterface) {} async createResponse(syncItemsResponse: SyncItemsResponse): Promise { const conflicts = syncItemsResponse.conflicts.filter( @@ -28,9 +28,7 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface const unsaved = [] for (const conflict of pickOutConflictsResult.unsavedItems) { unsaved.push({ - item: conflict.serverItem - ? await this.itemProjector.projectFull(conflict.serverItem) - : conflict.unsavedItem, + item: conflict.serverItem ? this.mapper.toProjection(conflict.serverItem) : conflict.unsavedItem, error: { tag: conflict.type, }, @@ -39,12 +37,12 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface const retrievedItems = [] for (const item of pickOutConflictsResult.retrievedItems) { - retrievedItems.push(await this.itemProjector.projectFull(item)) + retrievedItems.push(this.mapper.toProjection(item)) } const savedItems = [] for (const item of syncItemsResponse.savedItems) { - savedItems.push(await this.itemProjector.projectFull(item)) + savedItems.push(this.mapper.toProjection(item)) } return { @@ -64,16 +62,16 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface unsavedItems: Array retrievedItems: Array } { - const savedIds: Array = savedItems.map((savedItem: Item) => savedItem.uuid) - const retrievedIds: Array = retrievedItems.map((retrievedItem: Item) => retrievedItem.uuid) + const savedIds: Array = savedItems.map((savedItem: Item) => savedItem.id.toString()) + const retrievedIds: Array = retrievedItems.map((retrievedItem: Item) => retrievedItem.id.toString()) const conflictingIds = savedIds.filter((savedId) => retrievedIds.includes(savedId)) for (const conflictingId of conflictingIds) { - const savedItem = savedItems.find((item) => item.uuid === conflictingId) - const conflictedItem = retrievedItems.find((item) => item.uuid === conflictingId) + const savedItem = savedItems.find((item) => item.id.toString() === conflictingId) + const conflictedItem = 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) { 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 { diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts index b19b74f12..edc5a7505 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.spec.ts @@ -1,43 +1,44 @@ import 'reflect-metadata' -import { ProjectorInterface } from '../../../Projection/ProjectorInterface' + +import { MapperInterface } from '@standardnotes/domain-core' + import { Item } from '../Item' import { ItemConflict } from '../ItemConflict' -import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection' -import { ItemProjection } from '../../../Projection/ItemProjection' - 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', () => { - let itemProjector: ProjectorInterface - let savedItemProjector: ProjectorInterface - let itemConflictProjector: ProjectorInterface - let itemProjection: ItemProjection - let savedItemProjection: SavedItemProjection - let itemConflictProjection: ItemConflictProjection + let itemMapper: MapperInterface + let savedItemMapper: MapperInterface + let itemConflictMapper: MapperInterface + let itemProjection: ItemHttpRepresentation + let savedItemHttpRepresentation: SavedItemHttpRepresentation + let itemConflictProjection: ItemConflictHttpRepresentation let item1: Item let item2: Item let itemConflict: ItemConflict - const createFactory = () => new SyncResponseFactory20200115(itemProjector, itemConflictProjector, savedItemProjector) + const createFactory = () => new SyncResponseFactory20200115(itemMapper, itemConflictMapper, savedItemMapper) beforeEach(() => { itemProjection = { uuid: '2-3-4', - } as jest.Mocked + } as jest.Mocked - itemProjector = {} as jest.Mocked> - itemProjector.projectFull = jest.fn().mockReturnValue(itemProjection) + itemMapper = {} as jest.Mocked> + itemMapper.toProjection = jest.fn().mockReturnValue(itemProjection) - itemConflictProjector = {} as jest.Mocked> - itemConflictProjector.projectFull = jest.fn().mockReturnValue(itemConflictProjection) + itemConflictMapper = {} as jest.Mocked> + itemConflictMapper.toProjection = jest.fn().mockReturnValue(itemConflictProjection) - savedItemProjection = { + savedItemHttpRepresentation = { uuid: '1-2-3', - } as jest.Mocked + } as jest.Mocked - savedItemProjector = {} as jest.Mocked> - savedItemProjector.projectFull = jest.fn().mockReturnValue(savedItemProjection) + savedItemMapper = {} as jest.Mocked> + savedItemMapper.toProjection = jest.fn().mockReturnValue(savedItemHttpRepresentation) item1 = {} as jest.Mocked @@ -57,7 +58,7 @@ describe('SyncResponseFactory20200115', () => { }), ).toEqual({ retrieved_items: [itemProjection], - saved_items: [savedItemProjection], + saved_items: [savedItemHttpRepresentation], conflicts: [itemConflictProjection], sync_token: 'sync-test', cursor_token: 'cursor-test', diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts index 8eb5960e3..e5a38ee7b 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts +++ b/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 { ItemConflict } from '../ItemConflict' -import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection' -import { ItemProjection } from '../../../Projection/ItemProjection' import { SyncResponse20200115 } from './SyncResponse20200115' import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface' -import { SavedItemProjection } from '../../../Projection/SavedItemProjection' 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 { constructor( - private itemProjector: ProjectorInterface, - private itemConflictProjector: ProjectorInterface, - private savedItemProjector: ProjectorInterface, + private httpMapper: MapperInterface, + private itemConflictMapper: MapperInterface, + private savedItemMapper: MapperInterface, ) {} async createResponse(syncItemsResponse: SyncItemsResponse): Promise { const retrievedItems = [] for (const item of syncItemsResponse.retrievedItems) { - retrievedItems.push(await this.itemProjector.projectFull(item)) + retrievedItems.push(this.httpMapper.toProjection(item)) } const savedItems = [] for (const item of syncItemsResponse.savedItems) { - savedItems.push(await this.savedItemProjector.projectFull(item)) + savedItems.push(this.savedItemMapper.toProjection(item)) } const conflicts = [] for (const itemConflict of syncItemsResponse.conflicts) { - conflicts.push(await this.itemConflictProjector.projectFull(itemConflict)) + conflicts.push(this.itemConflictMapper.toProjection(itemConflict)) } return { diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.spec.ts index 1bb196f59..df4cb9dcc 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.spec.ts @@ -1,9 +1,10 @@ import 'reflect-metadata' +import { ContentType } from '@standardnotes/domain-core' + import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface' import { CheckIntegrity } from './CheckIntegrity' -import { ContentType } from '@standardnotes/common' describe('CheckIntegrity', () => { let itemRepository: ItemRepositoryInterface @@ -16,27 +17,27 @@ describe('CheckIntegrity', () => { { uuid: '1-2-3', updated_at_timestamp: 1, - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, { uuid: '2-3-4', updated_at_timestamp: 2, - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, { uuid: '3-4-5', updated_at_timestamp: 3, - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, }, { uuid: '4-5-6', updated_at_timestamp: 4, - content_type: ContentType.ItemsKey, + content_type: ContentType.TYPES.ItemsKey, }, { uuid: '5-6-7', updated_at_timestamp: 5, - content_type: ContentType.File, + content_type: ContentType.TYPES.File, }, ]) }) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.ts index 49c61cc5f..439b9098a 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity.ts @@ -1,6 +1,5 @@ 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 { CheckIntegrityDTO } from './CheckIntegrityDTO' @@ -33,7 +32,7 @@ export class CheckIntegrity implements UseCaseInterface { ) as ExtendedIntegrityPayload if (!clientItemIntegrityPayloadsMap.has(serverItemIntegrityPayloadUuid)) { - if (serverItemIntegrityPayload.content_type !== ContentType.ItemsKey) { + if (serverItemIntegrityPayload.content_type !== ContentType.TYPES.ItemsKey) { mismatches.unshift({ uuid: serverItemIntegrityPayloadUuid, updated_at_timestamp: serverItemIntegrityPayload.updated_at_timestamp, @@ -49,7 +48,7 @@ export class CheckIntegrity implements UseCaseInterface { ) as number if ( serverItemIntegrityPayloadUpdatedAtTimestamp !== clientItemIntegrityPayloadUpdatedAtTimestamp && - serverItemIntegrityPayload.content_type !== ContentType.ItemsKey + serverItemIntegrityPayload.content_type !== ContentType.TYPES.ItemsKey ) { mismatches.unshift({ uuid: serverItemIntegrityPayloadUuid, diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.spec.ts new file mode 100644 index 000000000..21abecf08 --- /dev/null +++ b/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 + + itemRepository = {} as jest.Mocked + itemRepository.save = jest.fn() + + timer = {} as jest.Mocked + 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 + domainEventPublisher.publish = jest.fn() + + domainEventFactory = {} as jest.Mocked + domainEventFactory.createDuplicateItemSyncedEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) + domainEventFactory.createItemRevisionCreationRequested = jest + .fn() + .mockReturnValue({} as jest.Mocked) + }) + + 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() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts new file mode 100644 index 000000000..3cd1fef94 --- /dev/null +++ b/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 { + constructor( + private itemRepository: ItemRepositoryInterface, + private timer: TimerInterface, + private domainEventPublisher: DomainEventPublisherInterface, + private domainEventFactory: DomainEventFactoryInterface, + ) {} + + async execute(dto: SaveNewItemDTO): Promise> { + 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) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItemDTO.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItemDTO.ts new file mode 100644 index 000000000..3696d94fc --- /dev/null +++ b/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 +} diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts index 230730f39..b9d9cf0fb 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts @@ -1,13 +1,12 @@ import 'reflect-metadata' -import { ContentType } from '@standardnotes/common' - import { ApiVersion } from '../../../Api/ApiVersion' import { Item } from '../../../Item/Item' import { ItemHash } from '../../../Item/ItemHash' import { ItemServiceInterface } from '../../../Item/ItemServiceInterface' import { SyncItems } from './SyncItems' +import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core' describe('SyncItems', () => { let itemService: ItemServiceInterface @@ -19,20 +18,59 @@ describe('SyncItems', () => { const createUseCase = () => new SyncItems(itemService) beforeEach(() => { - item1 = { - uuid: '1-2-3', - } as jest.Mocked - item2 = { - uuid: '2-3-4', - } as jest.Mocked - item3 = { - uuid: '3-4-5', - } as jest.Mocked + 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 = { uuid: '2-3-4', content: 'asdqwe', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, duplicate_of: null, enc_item_key: 'qweqwe', items_key_id: 'asdasd', diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts index 82b96c431..15f3ae2d8 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts @@ -1,4 +1,5 @@ import { Result, UseCaseInterface } from '@standardnotes/domain-core' + import { Item } from '../../../Item/Item' import { ItemConflict } from '../../../Item/ItemConflict' import { ItemServiceInterface } from '../../../Item/ItemServiceInterface' @@ -52,10 +53,10 @@ export class SyncItems implements UseCaseInterface { const syncConflictIds: Array = [] conflicts.forEach((conflict: ItemConflict) => { 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) } } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts new file mode 100644 index 000000000..a5fc81095 --- /dev/null +++ b/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 + + itemRepository = {} as jest.Mocked + itemRepository.save = jest.fn() + + timer = {} as jest.Mocked + 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 + domainEventPublisher.publish = jest.fn() + + domainEventFactory = {} as jest.Mocked + domainEventFactory.createDuplicateItemSyncedEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) + domainEventFactory.createItemRevisionCreationRequested = jest + .fn() + .mockReturnValue({} as jest.Mocked) + }) + + 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() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts new file mode 100644 index 000000000..ad79cd099 --- /dev/null +++ b/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 { + constructor( + private itemRepository: ItemRepositoryInterface, + private timer: TimerInterface, + private domainEventPublisher: DomainEventPublisherInterface, + private domainEventFactory: DomainEventFactoryInterface, + private revisionFrequency: number, + ) {} + + async execute(dto: UpdateExistingItemDTO): Promise> { + 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) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItemDTO.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItemDTO.ts new file mode 100644 index 000000000..61b0379c5 --- /dev/null +++ b/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 +} diff --git a/packages/syncing-server/src/Infra/FS/FSItemBackupService.ts b/packages/syncing-server/src/Infra/FS/FSItemBackupService.ts index 451f4f77e..abec72d37 100644 --- a/packages/syncing-server/src/Infra/FS/FSItemBackupService.ts +++ b/packages/syncing-server/src/Infra/FS/FSItemBackupService.ts @@ -1,4 +1,5 @@ import { KeyParamsData } from '@standardnotes/responses' +import { MapperInterface } from '@standardnotes/domain-core' import { promises } from 'fs' import * as uuid from 'uuid' import { Logger } from 'winston' @@ -6,13 +7,12 @@ import { dirname } from 'path' import { Item } from '../../Domain/Item/Item' 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 { constructor( private fileUploadPath: string, - private itemProjector: ProjectorInterface, + private mapper: MapperInterface, private logger: Logger, ) {} @@ -22,12 +22,12 @@ export class FSItemBackupService implements ItemBackupServiceInterface { async dump(item: Item): Promise { const contents = JSON.stringify({ - item: await this.itemProjector.projectCustom('dump', item), + item: this.mapper.toProjection(item), }) 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 }) diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController.ts index db1836890..8a5e8a4b0 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController.ts +++ b/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 { Request, Response } from 'express' @@ -6,18 +6,17 @@ import { Item } from '../../../Domain/Item/Item' import { SyncResponseFactoryResolverInterface } from '../../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface' import { CheckIntegrity } from '../../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity' 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 { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems' import { HttpStatusCode } from '@standardnotes/responses' +import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation' export class HomeServerItemsController extends BaseHttpController { constructor( protected syncItems: SyncItems, protected checkIntegrity: CheckIntegrity, protected getItem: GetItem, - protected itemProjector: ProjectorInterface, + protected itemHttpMapper: MapperInterface, protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface, private controllerContainer?: ControllerContainerInterface, ) { @@ -92,6 +91,6 @@ export class HomeServerItemsController extends BaseHttpController { 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()) }) } } diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts index 66ede6da9..4abf360c8 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts @@ -1,14 +1,11 @@ import 'reflect-metadata' 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 { InversifyExpressItemsController } from './InversifyExpressItemsController' import { Item } from '../../Domain/Item/Item' -import { ItemProjection } from '../../Projection/ItemProjection' -import { ProjectorInterface } from '../../Projection/ProjectorInterface' import { ApiVersion } from '../../Domain/Api/ApiVersion' import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115' 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 { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem' import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems' +import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation' describe('InversifyExpressItemsController', () => { let syncItems: SyncItems let checkIntegrity: CheckIntegrity let getItem: GetItem - let itemProjector: ProjectorInterface + let mapper: MapperInterface let request: express.Request let response: express.Response let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface @@ -29,11 +27,11 @@ describe('InversifyExpressItemsController', () => { let syncResponse: SyncResponse20200115 const createController = () => - new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, itemProjector, syncResponceFactoryResolver) + new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, mapper, syncResponceFactoryResolver) beforeEach(() => { - itemProjector = {} as jest.Mocked> - itemProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' }) + mapper = {} as jest.Mocked> + mapper.toProjection = jest.fn().mockReturnValue({ foo: 'bar' }) syncItems = {} as jest.Mocked syncItems.execute = jest.fn().mockReturnValue(Result.ok({ foo: 'bar' })) @@ -58,7 +56,7 @@ describe('InversifyExpressItemsController', () => { request.body.items = [ { content: 'test', - content_type: ContentType.Note, + content_type: ContentType.TYPES.Note, created_at: '2021-02-19T11:35:45.655Z', deleted: false, duplicate_of: null, diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.ts index b15bead42..c6b29e57a 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.ts +++ b/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 { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem' import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems' -import { ItemProjection } from '../../Projection/ItemProjection' -import { ProjectorInterface } from '../../Projection/ProjectorInterface' import { HomeServerItemsController } from './HomeServer/HomeServerItemsController' +import { MapperInterface } from '@standardnotes/domain-core' +import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation' @controller('/items', TYPES.Sync_AuthMiddleware) export class InversifyExpressItemsController extends HomeServerItemsController { @@ -18,11 +18,11 @@ export class InversifyExpressItemsController extends HomeServerItemsController { @inject(TYPES.Sync_SyncItems) override syncItems: SyncItems, @inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity, @inject(TYPES.Sync_GetItem) override getItem: GetItem, - @inject(TYPES.Sync_ItemProjector) override itemProjector: ProjectorInterface, + @inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface, @inject(TYPES.Sync_SyncResponseFactoryResolver) override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface, ) { - super(syncItems, checkIntegrity, getItem, itemProjector, syncResponseFactoryResolver) + super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver) } @httpPost('/sync') diff --git a/packages/syncing-server/src/Infra/S3/S3ItemBackupService.ts b/packages/syncing-server/src/Infra/S3/S3ItemBackupService.ts index 3c8ea686a..cc7329fd3 100644 --- a/packages/syncing-server/src/Infra/S3/S3ItemBackupService.ts +++ b/packages/syncing-server/src/Infra/S3/S3ItemBackupService.ts @@ -5,13 +5,15 @@ import { Logger } from 'winston' import { Item } from '../../Domain/Item/Item' 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 { constructor( private s3BackupBucketName: string, - private itemProjector: ProjectorInterface, + private backupMapper: MapperInterface, + private httpMapper: MapperInterface, private logger: Logger, private s3Client?: S3Client, ) {} @@ -29,7 +31,7 @@ export class S3ItemBackupService implements ItemBackupServiceInterface { Bucket: this.s3BackupBucketName, Key: s3Key, 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 = [] - let itemProjections: Array = [] + let itemProjections: Array = [] let contentSizeCounter = 0 for (const item of items) { - const itemProjection = await this.itemProjector.projectFull(item) + const itemProjection = this.httpMapper.toProjection(item) if (contentSizeLimit === undefined) { itemProjections.push(itemProjection) @@ -79,7 +81,10 @@ export class S3ItemBackupService implements ItemBackupServiceInterface { return fileNames } - private async createBackupFile(itemProjections: ItemProjection[], authParams: KeyParamsData): Promise { + private async createBackupFile( + itemRepresentations: ItemHttpRepresentation[], + authParams: KeyParamsData, + ): Promise { const fileName = uuid.v4() await (this.s3Client as S3Client).send( @@ -87,7 +92,7 @@ export class S3ItemBackupService implements ItemBackupServiceInterface { Bucket: this.s3BackupBucketName, Key: fileName, Body: JSON.stringify({ - items: itemProjections, + items: itemRepresentations, auth_params: authParams, }), }), diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMItem.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMItem.ts new file mode 100644 index 000000000..d3b1f5815 --- /dev/null +++ b/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 +} diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts index 2bf129c8e..44c7caec0 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts @@ -1,19 +1,24 @@ +import { ReadStream } from 'fs' import { Repository, SelectQueryBuilder } from 'typeorm' +import { MapperInterface } from '@standardnotes/domain-core' + import { Item } from '../../Domain/Item/Item' import { ItemQuery } from '../../Domain/Item/ItemQuery' import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface' -import { ReadStream } from 'fs' import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload' +import { TypeORMItem } from './TypeORMItem' export class TypeORMItemRepository implements ItemRepositoryInterface { - constructor(private ormRepository: Repository) {} + constructor(private ormRepository: Repository, private mapper: MapperInterface) {} - async save(item: Item): Promise { - return this.ormRepository.save(item) + async save(item: Item): Promise { + const persistence = this.mapper.toProjection(item) + + await this.ormRepository.save(persistence) } - async remove(item: Item): Promise { - return this.ormRepository.remove(item) + async remove(item: Item): Promise { + await this.ormRepository.remove(this.mapper.toProjection(item)) } async updateContentSize(itemUuid: string, contentSize: number): Promise { @@ -51,12 +56,18 @@ export class TypeORMItemRepository implements ItemRepositoryInterface { } async findByUuid(uuid: string): Promise { - return this.ormRepository + const persistence = await this.ormRepository .createQueryBuilder('item') .where('item.uuid = :uuid', { uuid, }) .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) } async findDatesForComputingIntegrityHash(userUuid: string): Promise> { @@ -84,17 +95,25 @@ export class TypeORMItemRepository implements ItemRepositoryInterface { } async findByUuidAndUserUuid(uuid: string, userUuid: string): Promise { - return this.ormRepository + const persistence = await this.ormRepository .createQueryBuilder('item') .where('item.uuid = :uuid AND item.user_uuid = :userUuid', { uuid, userUuid, }) .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) } async findAll(query: ItemQuery): Promise { - return this.createFindAllQueryBuilder(query).getMany() + const persistence = await this.createFindAllQueryBuilder(query).getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) } async findAllRaw(query: ItemQuery): Promise { @@ -126,7 +145,7 @@ export class TypeORMItemRepository implements ItemRepositoryInterface { .execute() } - private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder { + private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder { const queryBuilder = this.ormRepository.createQueryBuilder('item') if (query.sortBy !== undefined && query.sortOrder !== undefined) { diff --git a/packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts b/packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts new file mode 100644 index 000000000..d558bb628 --- /dev/null +++ b/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 { + 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, + } + } +} diff --git a/packages/syncing-server/src/Mapping/Backup/ItemBackupRepresentation.ts b/packages/syncing-server/src/Mapping/Backup/ItemBackupRepresentation.ts new file mode 100644 index 000000000..68b833926 --- /dev/null +++ b/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 +} diff --git a/packages/syncing-server/src/Mapping/Http/ItemConflictHttpMapper.ts b/packages/syncing-server/src/Mapping/Http/ItemConflictHttpMapper.ts new file mode 100644 index 000000000..2f9f9a089 --- /dev/null +++ b/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 { + constructor(private mapper: MapperInterface) {} + + 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 + } +} diff --git a/packages/syncing-server/src/Mapping/Http/ItemConflictHttpRepresentation.ts b/packages/syncing-server/src/Mapping/Http/ItemConflictHttpRepresentation.ts new file mode 100644 index 000000000..9194f22dd --- /dev/null +++ b/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 +} diff --git a/packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts b/packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts new file mode 100644 index 000000000..b015d7fcc --- /dev/null +++ b/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 { + 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, + } + } +} diff --git a/packages/syncing-server/src/Projection/ItemProjection.ts b/packages/syncing-server/src/Mapping/Http/ItemHttpRepresentation.ts similarity index 89% rename from packages/syncing-server/src/Projection/ItemProjection.ts rename to packages/syncing-server/src/Mapping/Http/ItemHttpRepresentation.ts index ec1fbc8d5..9bd5184bf 100644 --- a/packages/syncing-server/src/Projection/ItemProjection.ts +++ b/packages/syncing-server/src/Mapping/Http/ItemHttpRepresentation.ts @@ -1,4 +1,4 @@ -export type ItemProjection = { +export interface ItemHttpRepresentation { uuid: string items_key_id: string | null duplicate_of: string | null diff --git a/packages/syncing-server/src/Mapping/Http/SavedItemHttpMapper.ts b/packages/syncing-server/src/Mapping/Http/SavedItemHttpMapper.ts new file mode 100644 index 000000000..a54ff20e1 --- /dev/null +++ b/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 { + 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, + } + } +} diff --git a/packages/syncing-server/src/Projection/SavedItemProjection.ts b/packages/syncing-server/src/Mapping/Http/SavedItemHttpRepresentation.ts similarity index 82% rename from packages/syncing-server/src/Projection/SavedItemProjection.ts rename to packages/syncing-server/src/Mapping/Http/SavedItemHttpRepresentation.ts index b8f4a99e3..8c77a6b9f 100644 --- a/packages/syncing-server/src/Projection/SavedItemProjection.ts +++ b/packages/syncing-server/src/Mapping/Http/SavedItemHttpRepresentation.ts @@ -1,4 +1,4 @@ -export type SavedItemProjection = { +export interface SavedItemHttpRepresentation { uuid: string duplicate_of: string | null content_type: string diff --git a/packages/syncing-server/src/Mapping/Persistence/ItemPersistenceMapper.ts b/packages/syncing-server/src/Mapping/Persistence/ItemPersistenceMapper.ts new file mode 100644 index 000000000..967ec4cf4 --- /dev/null +++ b/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 { + 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 + } +} diff --git a/packages/syncing-server/src/Projection/ItemConflictProjection.ts b/packages/syncing-server/src/Projection/ItemConflictProjection.ts deleted file mode 100644 index 374f13593..000000000 --- a/packages/syncing-server/src/Projection/ItemConflictProjection.ts +++ /dev/null @@ -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 -} diff --git a/packages/syncing-server/src/Projection/ItemConflictProjector.spec.ts b/packages/syncing-server/src/Projection/ItemConflictProjector.spec.ts deleted file mode 100644 index c6067a9d7..000000000 --- a/packages/syncing-server/src/Projection/ItemConflictProjector.spec.ts +++ /dev/null @@ -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 - 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 - - itemProjector = {} as jest.Mocked> - itemProjector.projectFull = jest.fn().mockReturnValue(itemProjection) - - item = {} as jest.Mocked - - itemHash = {} as jest.Mocked - - 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') - }) -}) diff --git a/packages/syncing-server/src/Projection/ItemConflictProjector.ts b/packages/syncing-server/src/Projection/ItemConflictProjector.ts deleted file mode 100644 index 5ec4bc728..000000000 --- a/packages/syncing-server/src/Projection/ItemConflictProjector.ts +++ /dev/null @@ -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 { - constructor(private itemProjector: ProjectorInterface) {} - - async projectSimple(_itemConflict: ItemConflict): Promise { - throw Error('not implemented') - } - - async projectCustom(_projectionType: string, _itemConflict: ItemConflict): Promise { - throw Error('not implemented') - } - - async projectFull(itemConflict: ItemConflict): Promise { - const projection: ItemConflictProjection = { - unsaved_item: itemConflict.unsavedItem, - type: itemConflict.type, - } - - if (itemConflict.serverItem) { - projection.server_item = await this.itemProjector.projectFull(itemConflict.serverItem) - } - - return projection - } -} diff --git a/packages/syncing-server/src/Projection/ItemProjectionWithUser.ts b/packages/syncing-server/src/Projection/ItemProjectionWithUser.ts deleted file mode 100644 index 66ba0095e..000000000 --- a/packages/syncing-server/src/Projection/ItemProjectionWithUser.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ItemProjection } from './ItemProjection' - -export type ItemProjectionWithUser = ItemProjection & { - user_uuid: string -} diff --git a/packages/syncing-server/src/Projection/ItemProjector.spec.ts b/packages/syncing-server/src/Projection/ItemProjector.spec.ts deleted file mode 100644 index 9876b1f84..000000000 --- a/packages/syncing-server/src/Projection/ItemProjector.spec.ts +++ /dev/null @@ -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 - 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') - }) -}) diff --git a/packages/syncing-server/src/Projection/ItemProjector.ts b/packages/syncing-server/src/Projection/ItemProjector.ts deleted file mode 100644 index f011a1124..000000000 --- a/packages/syncing-server/src/Projection/ItemProjector.ts +++ /dev/null @@ -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 { - constructor(private timer: TimerInterface) {} - - async projectSimple(_item: Item): Promise { - throw Error('not implemented') - } - - async projectCustom(_projectionType: string, item: Item): Promise { - const fullProjection = await this.projectFull(item) - - return { - ...fullProjection, - user_uuid: item.userUuid, - } - } - - async projectFull(item: Item): Promise { - 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, - } - } -} diff --git a/packages/syncing-server/src/Projection/ProjectorInterface.ts b/packages/syncing-server/src/Projection/ProjectorInterface.ts deleted file mode 100644 index 4ff2a693a..000000000 --- a/packages/syncing-server/src/Projection/ProjectorInterface.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ProjectorInterface { - projectSimple(object: T): Promise> - projectFull(object: T): Promise - projectCustom(projectionType: string, object: T, ...args: any[]): Promise -} diff --git a/packages/syncing-server/src/Projection/SavedItemProjector.spec.ts b/packages/syncing-server/src/Projection/SavedItemProjector.spec.ts deleted file mode 100644 index 6f7d42099..000000000 --- a/packages/syncing-server/src/Projection/SavedItemProjector.spec.ts +++ /dev/null @@ -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 - 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') - }) -}) diff --git a/packages/syncing-server/src/Projection/SavedItemProjector.ts b/packages/syncing-server/src/Projection/SavedItemProjector.ts deleted file mode 100644 index ac17308d9..000000000 --- a/packages/syncing-server/src/Projection/SavedItemProjector.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TimerInterface } from '@standardnotes/time' - -import { Item } from '../Domain/Item/Item' - -import { ProjectorInterface } from './ProjectorInterface' -import { SavedItemProjection } from './SavedItemProjection' - -export class SavedItemProjector implements ProjectorInterface { - constructor(private timer: TimerInterface) {} - - async projectSimple(_item: Item): Promise { - throw Error('not implemented') - } - - async projectCustom(_projectionType: string, _item: Item): Promise { - throw Error('not implemented') - } - - async projectFull(item: Item): Promise { - return { - uuid: item.uuid, - duplicate_of: item.duplicateOf, - 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, - } - } -} diff --git a/packages/websockets/package.json b/packages/websockets/package.json index cc62d7471..27f344ec4 100644 --- a/packages/websockets/package.json +++ b/packages/websockets/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@aws-sdk/client-sqs": "^3.332.0", - "@standardnotes/api": "^1.25.3", + "@standardnotes/api": "^1.26.25", "@standardnotes/common": "workspace:^", "@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-events": "workspace:^", diff --git a/yarn.lock b/yarn.lock index b72ca3295..564a4ea79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3511,19 +3511,18 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/api@npm:^1.25.3": - version: 1.26.10 - resolution: "@standardnotes/api@npm:1.26.10" +"@standardnotes/api@npm:^1.26.25": + version: 1.26.25 + resolution: "@standardnotes/api@npm:1.26.25" dependencies: - "@standardnotes/common": "npm:^1.46.6" + "@standardnotes/common": "npm:^1.48.3" "@standardnotes/domain-core": "npm:^1.12.0" - "@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": "npm:^1.7.6" - "@standardnotes/utils": "npm:1.16.5" + "@standardnotes/utils": "npm:1.17.4" reflect-metadata: "npm:^0.1.13" - checksum: 3c3561aec8edfb20549ad497c4cb1c287363385ec40a6aa90f8ad6a23a3f2bd53803f310c5bd31ec7a771c507aab5f9da99cd8446fd001be40c9764276e17787 + checksum: 68a820bd3642e78b09036aa6c7958c24baa5f77d05a63c63adf374a7edf844f15e2e49357956af9bd520681e9d698e23e99affb8bff61d8e510296f63d3af09b languageName: node linkType: hard @@ -3538,7 +3537,7 @@ __metadata: "@newrelic/winston-enricher": "npm:^4.0.1" "@simplewebauthn/server": "npm:^7.2.0" "@simplewebauthn/typescript-types": "npm:^7.0.0" - "@standardnotes/api": "npm:^1.25.3" + "@standardnotes/api": "npm:^1.26.25" "@standardnotes/common": "workspace:*" "@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-events": "workspace:*" @@ -3601,7 +3600,7 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/common@npm:^1.46.6, @standardnotes/common@workspace:*, @standardnotes/common@workspace:^, @standardnotes/common@workspace:packages/common": +"@standardnotes/common@npm:^1.46.6, @standardnotes/common@npm:^1.48.3, @standardnotes/common@workspace:*, @standardnotes/common@workspace:^, @standardnotes/common@workspace:packages/common": version: 0.0.0-use.local resolution: "@standardnotes/common@workspace:packages/common" dependencies: @@ -3687,20 +3686,6 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/encryption@npm:1.21.38": - version: 1.21.38 - resolution: "@standardnotes/encryption@npm:1.21.38" - dependencies: - "@standardnotes/common": "npm:^1.46.6" - "@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" - checksum: 13938405235e7e8cf22fec7cb850cd5515dccd083843d54784e4d058632d0c3b47a73f4def75a50b25af7b84b9593d4d5320479f3d9ba08f47d61fdf13706658 - languageName: node - linkType: hard - "@standardnotes/event-store@workspace:packages/event-store": version: 0.0.0-use.local resolution: "@standardnotes/event-store@workspace:packages/event-store" @@ -3750,6 +3735,18 @@ __metadata: languageName: node linkType: hard +"@standardnotes/features@npm:1.59.6": + version: 1.59.6 + resolution: "@standardnotes/features@npm:1.59.6" + dependencies: + "@standardnotes/common": "npm:^1.48.3" + "@standardnotes/domain-core": "npm:^1.12.0" + "@standardnotes/security": "npm:^1.7.6" + reflect-metadata: "npm:^0.1.13" + checksum: 2c855396f7e780f2929280b52955eebcc511b5e0df2cc912fa17fb81f652c0ef458d71f39c2c3b63a3baa77b9687bdc0012e501e95dbe3485ba88ca42f805258 + languageName: node + linkType: hard + "@standardnotes/files-server@workspace:^, @standardnotes/files-server@workspace:packages/files": version: 0.0.0-use.local resolution: "@standardnotes/files-server@workspace:packages/files" @@ -3842,16 +3839,17 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/models@npm:1.45.5": - version: 1.45.5 - resolution: "@standardnotes/models@npm:1.45.5" +"@standardnotes/models@npm:1.46.7": + version: 1.46.7 + resolution: "@standardnotes/models@npm:1.46.7" dependencies: - "@standardnotes/common": "npm:^1.46.6" - "@standardnotes/features": "npm:1.59.5" - "@standardnotes/responses": "npm:1.13.24" - "@standardnotes/utils": "npm:^1.16.5" + "@standardnotes/common": "npm:^1.48.3" + "@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" - checksum: 15f26c11b22604c35d7a89452d655b7745c2b0a21aacb66d3deda22ae47df252984c2b3a6b1c80998c3006b73df4b40dc750cd8dbd6341b76cbd1bb2f2fa3e77 + checksum: 50589454f1edf1e85e335851bf49796b6106f8968eac1b7d50a7662e51d55f65b37da78adb9ac74aba0a6119698befad1e0f04bd12d9de0f92ff7d377650b7e1 languageName: node linkType: hard @@ -3871,7 +3869,19 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/responses@npm:1.13.24, @standardnotes/responses@npm:^1.13.9": +"@standardnotes/responses@npm:1.13.26": + version: 1.13.26 + resolution: "@standardnotes/responses@npm:1.13.26" + dependencies: + "@standardnotes/common": "npm:^1.48.3" + "@standardnotes/features": "npm:1.59.6" + "@standardnotes/security": "npm:^1.7.6" + reflect-metadata: "npm:^0.1.13" + checksum: 6c5e3bf896a6f8fd2db2803938de120e3616ae34d0b3316cd18c2e5791970dd697441152b94c0ff6c2a1dc5157b07ac9864d3d1c5e9421debfc504defc98200d + languageName: node + linkType: hard + +"@standardnotes/responses@npm:^1.13.9": version: 1.13.24 resolution: "@standardnotes/responses@npm:1.13.24" dependencies: @@ -3890,7 +3900,7 @@ __metadata: "@aws-sdk/client-s3": "npm:^3.332.0" "@aws-sdk/client-sqs": "npm:^3.332.0" "@newrelic/winston-enricher": "npm:^4.0.1" - "@standardnotes/api": "npm:^1.25.3" + "@standardnotes/api": "npm:^1.26.25" "@standardnotes/common": "workspace:^" "@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-events": "workspace:*" @@ -4041,7 +4051,16 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/sncrypto-common@npm:1.13.3, @standardnotes/sncrypto-common@npm:^1.9.0": +"@standardnotes/sncrypto-common@npm:^1.13.4": + version: 1.13.4 + resolution: "@standardnotes/sncrypto-common@npm:1.13.4" + dependencies: + reflect-metadata: "npm:^0.1.13" + checksum: 48e0e207f2755140469e858c149eeebd7e264e9c8e8abdf0a7029dda6261b60a69935254534a5403f9db6bfa132f5807e34c85707a1ac98247ee937f6b92019d + languageName: node + linkType: hard + +"@standardnotes/sncrypto-common@npm:^1.9.0": version: 1.13.3 resolution: "@standardnotes/sncrypto-common@npm:1.13.3" dependencies: @@ -4080,7 +4099,7 @@ __metadata: "@aws-sdk/client-sns": "npm:^3.332.0" "@aws-sdk/client-sqs": "npm:^3.332.0" "@newrelic/winston-enricher": "npm:^4.0.1" - "@standardnotes/api": "npm:^1.25.3" + "@standardnotes/api": "npm:^1.26.25" "@standardnotes/common": "workspace:*" "@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-events": "workspace:*" @@ -4159,7 +4178,19 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/utils@npm:1.16.5, @standardnotes/utils@npm:^1.16.2, @standardnotes/utils@npm:^1.16.5": +"@standardnotes/utils@npm:1.17.4, @standardnotes/utils@npm:^1.17.4": + version: 1.17.4 + resolution: "@standardnotes/utils@npm:1.17.4" + dependencies: + "@standardnotes/common": "npm:^1.48.3" + dompurify: "npm:^2.4.1" + lodash: "npm:^4.17.21" + reflect-metadata: "npm:^0.1.13" + checksum: 7cb3fc838dd4bedd23da64644b774b720aa3778080a77c342a7c06657bd810fd8721183fbe2ccaf4d255438da7cc880e18af5dcb4b524d8ebc056da659017ea7 + languageName: node + linkType: hard + +"@standardnotes/utils@npm:^1.16.2": version: 1.16.5 resolution: "@standardnotes/utils@npm:1.16.5" dependencies: @@ -4177,7 +4208,7 @@ __metadata: dependencies: "@aws-sdk/client-sqs": "npm:^3.332.0" "@newrelic/winston-enricher": "npm:^4.0.1" - "@standardnotes/api": "npm:^1.25.3" + "@standardnotes/api": "npm:^1.26.25" "@standardnotes/common": "workspace:^" "@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-events": "workspace:^"