feat: domain items (#655)
* feat: content type as a value object * feat: turn items into domain entities * fix: update @standardnotes/api * fix(syncing-server): bindings order
This commit is contained in:
parent
c970b1ea68
commit
a0af8f0025
103 changed files with 2275 additions and 1873 deletions
90
.pnp.cjs
generated
90
.pnp.cjs
generated
|
@ -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"],\
|
||||
|
|
Binary file not shown.
BIN
.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-api-npm-1.26.25-fbb86eb9b7-68a820bd36.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-features-npm-1.59.6-2bcea0cc35-2c855396f7.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-models-npm-1.46.7-ef9a3fc3ad-50589454f1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-responses-npm-1.13.26-cd12940788-6c5e3bf896.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.4-3186513fa6-48e0e207f2.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-utils-npm-1.17.4-e5908cc204-7cb3fc838d.zip
vendored
Normal file
Binary file not shown.
|
@ -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:*",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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<HttpResponse<UserUpdateResponse>> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
async deleteAccount(_params: never): Promise<HttpResponse<UserDeletionResponseBody>> {
|
||||
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<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
|
||||
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<HttpResponse<RecoveryKeyParamsResponseBody>> {
|
||||
if (params.apiVersion !== ApiVersion.v0) {
|
||||
if (params.apiVersion !== ApiVersion.v20200115) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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<AuthResponse20200115> {
|
||||
constructor(
|
||||
|
@ -100,7 +100,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
|
|||
|
||||
const authResponse = await this.authResponseFactory.createResponse({
|
||||
user,
|
||||
apiVersion: ApiVersion.v0,
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
userAgent: dto.userAgent,
|
||||
ephemeralSession: false,
|
||||
readonlyAccess: false,
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/* istanbul ignore file */
|
||||
export enum ContentType {
|
||||
Any = '*',
|
||||
Item = 'SF|Item',
|
||||
KeySystemItemsKey = 'SN|KeySystemItemsKey',
|
||||
KeySystemRootKey = 'SN|KeySystemRootKey',
|
||||
TrustedContact = 'SN|TrustedContact',
|
||||
VaultListing = 'SN|VaultListing',
|
||||
RootKey = 'SN|RootKey|NoSync',
|
||||
ItemsKey = 'SN|ItemsKey',
|
||||
EncryptedStorage = 'SN|EncryptedStorage',
|
||||
Privileges = 'SN|Privileges',
|
||||
Note = 'Note',
|
||||
Tag = 'Tag',
|
||||
SmartView = 'SN|SmartTag',
|
||||
Component = 'SN|Component',
|
||||
Editor = 'SN|Editor',
|
||||
ActionsExtension = 'Extension',
|
||||
UserPrefs = 'SN|UserPreferences',
|
||||
HistorySession = 'SN|HistorySession',
|
||||
Theme = 'SN|Theme',
|
||||
File = 'SN|File',
|
||||
FilesafeCredentials = 'SN|FileSafe|Credentials',
|
||||
FilesafeFileMetadata = 'SN|FileSafe|FileMetadata',
|
||||
FilesafeIntegration = 'SN|FileSafe|Integration',
|
||||
ExtensionRepo = 'SN|ExtensionRepo',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export function DisplayStringForContentType(contentType: ContentType): string | undefined {
|
||||
const map: Partial<Record<ContentType, string>> = {
|
||||
[ContentType.ActionsExtension]: 'action-based extension',
|
||||
[ContentType.Component]: 'component',
|
||||
[ContentType.Editor]: 'editor',
|
||||
[ContentType.File]: 'file',
|
||||
[ContentType.FilesafeCredentials]: 'FileSafe credential',
|
||||
[ContentType.FilesafeFileMetadata]: 'FileSafe file',
|
||||
[ContentType.FilesafeIntegration]: 'FileSafe integration',
|
||||
[ContentType.ItemsKey]: 'encryption key',
|
||||
[ContentType.Note]: 'note',
|
||||
[ContentType.SmartView]: 'smart view',
|
||||
[ContentType.Tag]: 'tag',
|
||||
[ContentType.Theme]: 'theme',
|
||||
[ContentType.UserPrefs]: 'user preferences',
|
||||
}
|
||||
|
||||
return map[contentType]
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
export * from './Content/ContentType'
|
||||
export * from './Content/ContentDecoder'
|
||||
export * from './Content/ContentDecoderInterface'
|
||||
export * from './DataType/AnyRecord'
|
||||
|
|
39
packages/domain-core/src/Domain/Common/ContentType.spec.ts
Normal file
39
packages/domain-core/src/Domain/Common/ContentType.spec.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { ContentType } from './ContentType'
|
||||
|
||||
describe('ContentType', () => {
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = ContentType.create(ContentType.TYPES.Component)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().value).toEqual('SN|Component')
|
||||
})
|
||||
|
||||
it('should not create an invalid value object', () => {
|
||||
for (const value of ['', undefined, 0, 'FOOBAR']) {
|
||||
const valueOrError = ContentType.create(value as string)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should return a display name', () => {
|
||||
const valueOrError = ContentType.create(ContentType.TYPES.FilesafeFileMetadata)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().getDisplayName()).toEqual('FileSafe file')
|
||||
})
|
||||
|
||||
it('should return null for a display name if the value is null', () => {
|
||||
const valueOrError = ContentType.create(null)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().getDisplayName()).toBeNull()
|
||||
})
|
||||
|
||||
it('should fallback to the value if the display name is not found', () => {
|
||||
const valueOrError = ContentType.create(ContentType.TYPES.Unknown)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().getDisplayName()).toEqual('Unknown')
|
||||
})
|
||||
})
|
79
packages/domain-core/src/Domain/Common/ContentType.ts
Normal file
79
packages/domain-core/src/Domain/Common/ContentType.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { Result } from '../Core/Result'
|
||||
import { ValueObject } from '../Core/ValueObject'
|
||||
|
||||
import { ContentTypeProps } from './ContentTypeProps'
|
||||
|
||||
export class ContentType extends ValueObject<ContentTypeProps> {
|
||||
static readonly TYPES = {
|
||||
Any: '*',
|
||||
Item: 'SF|Item',
|
||||
KeySystemItemsKey: 'SN|KeySystemItemsKey',
|
||||
KeySystemRootKey: 'SN|KeySystemRootKey',
|
||||
TrustedContact: 'SN|TrustedContact',
|
||||
VaultListing: 'SN|VaultListing',
|
||||
RootKey: 'SN|RootKey|NoSync',
|
||||
ItemsKey: 'SN|ItemsKey',
|
||||
EncryptedStorage: 'SN|EncryptedStorage',
|
||||
Privileges: 'SN|Privileges',
|
||||
Note: 'Note',
|
||||
Tag: 'Tag',
|
||||
SmartView: 'SN|SmartTag',
|
||||
Component: 'SN|Component',
|
||||
Editor: 'SN|Editor',
|
||||
ActionsExtension: 'Extension',
|
||||
UserPrefs: 'SN|UserPreferences',
|
||||
HistorySession: 'SN|HistorySession',
|
||||
Theme: 'SN|Theme',
|
||||
File: 'SN|File',
|
||||
FilesafeCredentials: 'SN|FileSafe|Credentials',
|
||||
FilesafeFileMetadata: 'SN|FileSafe|FileMetadata',
|
||||
FilesafeIntegration: 'SN|FileSafe|Integration',
|
||||
ExtensionRepo: 'SN|ExtensionRepo',
|
||||
Unknown: 'Unknown',
|
||||
}
|
||||
|
||||
private readonly displayNamesMap: Partial<Record<string, string>> = {
|
||||
[ContentType.TYPES.ActionsExtension]: 'action-based extension',
|
||||
[ContentType.TYPES.Component]: 'component',
|
||||
[ContentType.TYPES.Editor]: 'editor',
|
||||
[ContentType.TYPES.File]: 'file',
|
||||
[ContentType.TYPES.FilesafeCredentials]: 'FileSafe credential',
|
||||
[ContentType.TYPES.FilesafeFileMetadata]: 'FileSafe file',
|
||||
[ContentType.TYPES.FilesafeIntegration]: 'FileSafe integration',
|
||||
[ContentType.TYPES.ItemsKey]: 'encryption key',
|
||||
[ContentType.TYPES.Note]: 'note',
|
||||
[ContentType.TYPES.SmartView]: 'smart view',
|
||||
[ContentType.TYPES.Tag]: 'tag',
|
||||
[ContentType.TYPES.Theme]: 'theme',
|
||||
[ContentType.TYPES.UserPrefs]: 'user preferences',
|
||||
}
|
||||
|
||||
get value(): string | null {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
private constructor(props: ContentTypeProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(type: string | null): Result<ContentType> {
|
||||
if (type === null) {
|
||||
return Result.ok<ContentType>(new ContentType({ value: null }))
|
||||
}
|
||||
|
||||
const isValidType = Object.values(this.TYPES).includes(type)
|
||||
if (!isValidType) {
|
||||
return Result.fail<ContentType>(`Invalid content type: ${type}`)
|
||||
} else {
|
||||
return Result.ok<ContentType>(new ContentType({ value: type }))
|
||||
}
|
||||
}
|
||||
|
||||
getDisplayName(): string | null {
|
||||
if (!this.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.displayNamesMap[this.value] || this.value
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -1,22 +0,0 @@
|
|||
import { ContentType as ContentTypeValues } from '@standardnotes/common'
|
||||
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { ContentTypeProps } from './ContentTypeProps'
|
||||
|
||||
export class ContentType extends ValueObject<ContentTypeProps> {
|
||||
get value(): string | null {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
private constructor(props: ContentTypeProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(contentType: string | null): Result<ContentType> {
|
||||
if (contentType !== null && !Object.values(ContentTypeValues).includes(contentType as ContentTypeValues)) {
|
||||
return Result.fail<ContentType>(`Value is not a valid content type: ${contentType}`)
|
||||
} else {
|
||||
return Result.ok<ContentType>(new ContentType({ value: contentType }))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Revision, string> {
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -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<TimerInterface>(TYPES.Sync_Timer).toConstantValue(new Timer())
|
||||
|
||||
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
|
||||
|
||||
container.bind<Env>(TYPES.Sync_Env).toConstantValue(env)
|
||||
|
@ -201,24 +205,37 @@ export class ContainerConfigLoader {
|
|||
})
|
||||
}
|
||||
|
||||
// Repositories
|
||||
container.bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository).toDynamicValue((context: interfaces.Context) => {
|
||||
return new TypeORMItemRepository(context.container.get(TYPES.Sync_ORMItemRepository))
|
||||
})
|
||||
// Mapping
|
||||
container
|
||||
.bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
|
||||
.toConstantValue(new ItemPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper)
|
||||
.toConstantValue(new ItemHttpMapper(container.get(TYPES.Sync_Timer)))
|
||||
container
|
||||
.bind<MapperInterface<Item, SavedItemHttpRepresentation>>(TYPES.Sync_SavedItemHttpMapper)
|
||||
.toConstantValue(new SavedItemHttpMapper(container.get(TYPES.Sync_Timer)))
|
||||
container
|
||||
.bind<MapperInterface<ItemConflict, ItemConflictHttpRepresentation>>(TYPES.Sync_ItemConflictHttpMapper)
|
||||
.toConstantValue(new ItemConflictHttpMapper(container.get(TYPES.Sync_ItemHttpMapper)))
|
||||
container
|
||||
.bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
|
||||
.toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
|
||||
|
||||
// ORM
|
||||
container
|
||||
.bind<Repository<Item>>(TYPES.Sync_ORMItemRepository)
|
||||
.toDynamicValue(() => appDataSource.getRepository(Item))
|
||||
.bind<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository)
|
||||
.toDynamicValue(() => appDataSource.getRepository(TypeORMItem))
|
||||
|
||||
// Projectors
|
||||
// Repositories
|
||||
container
|
||||
.bind<ProjectorInterface<Item, ItemProjection>>(TYPES.Sync_ItemProjector)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemProjector(context.container.get(TYPES.Sync_Timer))
|
||||
})
|
||||
|
||||
container.bind<TimerInterface>(TYPES.Sync_Timer).toDynamicValue(() => new Timer())
|
||||
.bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMItemRepository(
|
||||
container.get(TYPES.Sync_ORMItemRepository),
|
||||
container.get(TYPES.Sync_ItemPersistenceMapper),
|
||||
),
|
||||
)
|
||||
|
||||
container
|
||||
.bind<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory)
|
||||
|
@ -245,18 +262,6 @@ export class ContainerConfigLoader {
|
|||
)
|
||||
})
|
||||
|
||||
// Projectors
|
||||
container
|
||||
.bind<ProjectorInterface<Item, SavedItemProjection>>(TYPES.Sync_SavedItemProjector)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new SavedItemProjector(context.container.get(TYPES.Sync_Timer))
|
||||
})
|
||||
container
|
||||
.bind<ProjectorInterface<ItemConflict, ItemConflictProjection>>(TYPES.Sync_ItemConflictProjector)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemConflictProjector(context.container.get(TYPES.Sync_ItemProjector))
|
||||
})
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||
container
|
||||
|
@ -287,60 +292,35 @@ export class ContainerConfigLoader {
|
|||
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
container
|
||||
.bind<SaveNewItem>(TYPES.Sync_SaveNewItem)
|
||||
.toConstantValue(
|
||||
new SaveNewItem(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_DomainEventPublisher),
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem)
|
||||
.toConstantValue(
|
||||
new UpdateExistingItem(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_DomainEventPublisher),
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
container.get(TYPES.Sync_REVISIONS_FREQUENCY),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container.bind<ItemServiceInterface>(TYPES.Sync_ItemService).toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemService(
|
||||
context.container.get(TYPES.Sync_ItemSaveValidator),
|
||||
context.container.get(TYPES.Sync_ItemFactory),
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_REVISIONS_FREQUENCY),
|
||||
context.container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
context.container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
context.container.get(TYPES.Sync_Timer),
|
||||
context.container.get(TYPES.Sync_ItemProjector),
|
||||
context.container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
container
|
||||
.bind<SyncResponseFactory20161215>(TYPES.Sync_SyncResponseFactory20161215)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new SyncResponseFactory20161215(context.container.get(TYPES.Sync_ItemProjector))
|
||||
})
|
||||
container
|
||||
.bind<SyncResponseFactory20200115>(TYPES.Sync_SyncResponseFactory20200115)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new SyncResponseFactory20200115(
|
||||
context.container.get(TYPES.Sync_ItemProjector),
|
||||
context.container.get(TYPES.Sync_ItemConflictProjector),
|
||||
context.container.get(TYPES.Sync_SavedItemProjector),
|
||||
)
|
||||
})
|
||||
container
|
||||
.bind<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new SyncResponseFactoryResolver(
|
||||
context.container.get(TYPES.Sync_SyncResponseFactory20161215),
|
||||
context.container.get(TYPES.Sync_SyncResponseFactory20200115),
|
||||
)
|
||||
})
|
||||
|
||||
container.bind<ItemFactoryInterface>(TYPES.Sync_ItemFactory).toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemFactory(context.container.get(TYPES.Sync_Timer), context.container.get(TYPES.Sync_ItemProjector))
|
||||
})
|
||||
|
||||
container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toDynamicValue(() => new OwnershipFilter())
|
||||
container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toConstantValue(new OwnershipFilter())
|
||||
container
|
||||
.bind<TimeDifferenceFilter>(TYPES.Sync_TimeDifferenceFilter)
|
||||
.toDynamicValue(
|
||||
(context: interfaces.Context) => new TimeDifferenceFilter(context.container.get(TYPES.Sync_Timer)),
|
||||
)
|
||||
container.bind<UuidFilter>(TYPES.Sync_UuidFilter).toDynamicValue(() => new UuidFilter())
|
||||
container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toDynamicValue(() => new ContentTypeFilter())
|
||||
container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toDynamicValue(() => new ContentFilter())
|
||||
.toConstantValue(new TimeDifferenceFilter(container.get(TYPES.Sync_Timer)))
|
||||
container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toConstantValue(new ContentTypeFilter())
|
||||
container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toConstantValue(new ContentFilter())
|
||||
|
||||
container
|
||||
.bind<ItemSaveValidatorInterface>(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<ItemServiceInterface>(TYPES.Sync_ItemService)
|
||||
.toConstantValue(
|
||||
new ItemService(
|
||||
container.get(TYPES.Sync_ItemSaveValidator),
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
|
||||
container.get(TYPES.Sync_SaveNewItem),
|
||||
container.get(TYPES.Sync_UpdateExistingItem),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SyncResponseFactory20161215>(TYPES.Sync_SyncResponseFactory20161215)
|
||||
.toConstantValue(new SyncResponseFactory20161215(container.get(TYPES.Sync_ItemHttpMapper)))
|
||||
container
|
||||
.bind<SyncResponseFactory20200115>(TYPES.Sync_SyncResponseFactory20200115)
|
||||
.toConstantValue(
|
||||
new SyncResponseFactory20200115(
|
||||
container.get(TYPES.Sync_ItemHttpMapper),
|
||||
container.get(TYPES.Sync_ItemConflictHttpMapper),
|
||||
container.get(TYPES.Sync_SavedItemHttpMapper),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new SyncResponseFactoryResolver(
|
||||
context.container.get(TYPES.Sync_SyncResponseFactory20161215),
|
||||
context.container.get(TYPES.Sync_SyncResponseFactory20200115),
|
||||
)
|
||||
})
|
||||
|
||||
// env vars
|
||||
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),
|
||||
),
|
||||
|
|
|
@ -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: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<AxiosInstance>
|
||||
httpClient.request = jest.fn().mockReturnValue({ status: 200, data: { foo: 'bar' } })
|
||||
|
||||
item = {
|
||||
content: 'test',
|
||||
} as jest.Mocked<Item>
|
||||
item = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
authParams = {} as jest.Mocked<KeyParamsData>
|
||||
|
||||
|
|
|
@ -140,11 +140,11 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
|
|||
email: string,
|
||||
): Promise<DomainEventInterface> {
|
||||
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)
|
||||
|
|
|
@ -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 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
|
|
|
@ -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<Item>
|
||||
originalItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
duplicateItem = {
|
||||
uuid: '2-3-4',
|
||||
duplicateOf: '1-2-3',
|
||||
} as jest.Mocked<Item>
|
||||
duplicateItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.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()
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,119 +1,22 @@
|
|||
import { ContentType } from '@standardnotes/common'
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
@Entity({ name: 'items' })
|
||||
@Index('index_items_on_user_uuid_and_content_type', ['userUuid', 'contentType'])
|
||||
@Index('user_uuid_and_updated_at_timestamp_and_created_at_timestamp', [
|
||||
'userUuid',
|
||||
'updatedAtTimestamp',
|
||||
'createdAtTimestamp',
|
||||
])
|
||||
@Index('user_uuid_and_deleted', ['userUuid', 'deleted'])
|
||||
export class Item {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
declare uuid: string
|
||||
import { ItemProps } from './ItemProps'
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
name: 'duplicate_of',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
})
|
||||
declare duplicateOf: string | null
|
||||
export class Item extends Entity<ItemProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
name: 'items_key_id',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
declare itemsKeyId: string | null
|
||||
private constructor(props: ItemProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
})
|
||||
declare content: string | null
|
||||
static create(props: ItemProps, id?: UniqueEntityId): Result<Item> {
|
||||
if (!props.contentSize) {
|
||||
const contentSize = Buffer.byteLength(JSON.stringify(props))
|
||||
props.contentSize = contentSize
|
||||
}
|
||||
|
||||
@Column({
|
||||
name: 'content_type',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
@Index('index_items_on_content_type')
|
||||
declare contentType: ContentType | null
|
||||
|
||||
@Column({
|
||||
name: 'content_size',
|
||||
type: 'int',
|
||||
nullable: true,
|
||||
})
|
||||
declare contentSize: number | null
|
||||
|
||||
@Column({
|
||||
name: 'enc_item_key',
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
})
|
||||
declare encItemKey: string | null
|
||||
|
||||
@Column({
|
||||
name: 'auth_hash',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
declare authHash: string | null
|
||||
|
||||
@Column({
|
||||
name: 'user_uuid',
|
||||
length: 36,
|
||||
})
|
||||
@Index('index_items_on_user_uuid')
|
||||
declare userUuid: string
|
||||
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
precision: 1,
|
||||
nullable: true,
|
||||
default: 0,
|
||||
})
|
||||
@Index('index_items_on_deleted')
|
||||
declare deleted: boolean
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
precision: 6,
|
||||
})
|
||||
declare createdAt: Date
|
||||
|
||||
@Column({
|
||||
name: 'updated_at',
|
||||
type: 'datetime',
|
||||
precision: 6,
|
||||
})
|
||||
declare updatedAt: Date
|
||||
|
||||
@Column({
|
||||
name: 'created_at_timestamp',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare createdAtTimestamp: number
|
||||
|
||||
@Column({
|
||||
name: 'updated_at_timestamp',
|
||||
type: 'bigint',
|
||||
})
|
||||
@Index('updated_at_timestamp')
|
||||
declare updatedAtTimestamp: number
|
||||
|
||||
@Column({
|
||||
name: 'updated_with_session',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
})
|
||||
declare updatedWithSession: string | null
|
||||
return Result.ok<Item>(new Item(props, id))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
import 'reflect-metadata'
|
||||
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
import { ItemFactory } from './ItemFactory'
|
||||
import { ItemHash } from './ItemHash'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { ItemProjection } from '../../Projection/ItemProjection'
|
||||
import { Item } from './Item'
|
||||
|
||||
describe('ItemFactory', () => {
|
||||
let timer: TimerInterface
|
||||
let itemProjector: ProjectorInterface<Item, ItemProjection>
|
||||
let timeHelper: Timer
|
||||
|
||||
const createFactory = () => new ItemFactory(timer, itemProjector)
|
||||
|
||||
beforeEach(() => {
|
||||
timeHelper = new Timer()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
|
||||
timer.convertMicrosecondsToDate = jest
|
||||
.fn()
|
||||
.mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds))
|
||||
timer.convertStringDateToMicroseconds = jest
|
||||
.fn()
|
||||
.mockImplementation((date: string) => timeHelper.convertStringDateToMicroseconds(date))
|
||||
timer.convertStringDateToDate = jest
|
||||
.fn()
|
||||
.mockImplementation((date: string) => timeHelper.convertStringDateToDate(date))
|
||||
|
||||
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
|
||||
itemProjector.projectFull = jest.fn().mockReturnValue({
|
||||
uuid: '1-2-3',
|
||||
items_key_id: 'foobar',
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'foobar',
|
||||
content: 'foobar',
|
||||
content_type: ContentType.Note,
|
||||
auth_hash: 'foobar',
|
||||
deleted: false,
|
||||
created_at: '2022-09-01 10:00:00',
|
||||
created_at_timestamp: 123123123123123,
|
||||
updated_at: '2022-09-01 10:00:00',
|
||||
updated_at_timestamp: 123123123123123,
|
||||
updated_with_session: '2-4-5',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an item based on item hash', () => {
|
||||
const itemHash = {
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const item = createFactory().create({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
|
||||
|
||||
expect(item).toEqual({
|
||||
createdAtTimestamp: 1616164633241568,
|
||||
createdAt: expect.any(Date),
|
||||
updatedWithSession: '1-2-3',
|
||||
updatedAt: expect.any(Date),
|
||||
updatedAtTimestamp: 1616164633241568,
|
||||
userUuid: 'a-b-c',
|
||||
uuid: '1-2-3',
|
||||
contentSize: 341,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a stub item based on item hash with update_at date and timestamps overwritten', () => {
|
||||
const itemHash = {
|
||||
uuid: '1-2-3',
|
||||
updated_at: '2021-03-25T09:37:37.943Z',
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const item = createFactory().createStub({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
|
||||
|
||||
expect(item).toEqual({
|
||||
createdAtTimestamp: 1616164633241568,
|
||||
createdAt: expect.any(Date),
|
||||
updatedWithSession: '1-2-3',
|
||||
updatedAt: new Date('2021-03-25T09:37:37.943Z'),
|
||||
updatedAtTimestamp: 1616665057943000,
|
||||
userUuid: 'a-b-c',
|
||||
uuid: '1-2-3',
|
||||
content: null,
|
||||
contentSize: 341,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a stub item based on item hash with update_at_timestamp date and timestamps overwritten', () => {
|
||||
const itemHash = {
|
||||
uuid: '1-2-3',
|
||||
updated_at_timestamp: 1616164633241568,
|
||||
content: 'foobar',
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const item = createFactory().createStub({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
|
||||
|
||||
expect(item).toEqual({
|
||||
createdAtTimestamp: 1616164633241568,
|
||||
createdAt: expect.any(Date),
|
||||
updatedWithSession: '1-2-3',
|
||||
updatedAt: new Date('2021-03-19T14:37:13.241Z'),
|
||||
updatedAtTimestamp: 1616164633241568,
|
||||
userUuid: 'a-b-c',
|
||||
uuid: '1-2-3',
|
||||
content: 'foobar',
|
||||
contentSize: 341,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a stub item based on item hash without updated timestamps', () => {
|
||||
const itemHash = {
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const item = createFactory().createStub({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
|
||||
|
||||
expect(item).toEqual({
|
||||
createdAtTimestamp: 1616164633241568,
|
||||
createdAt: expect.any(Date),
|
||||
updatedWithSession: '1-2-3',
|
||||
updatedAt: expect.any(Date),
|
||||
updatedAtTimestamp: 1616164633241568,
|
||||
userUuid: 'a-b-c',
|
||||
uuid: '1-2-3',
|
||||
content: null,
|
||||
contentSize: 341,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an item based on item hash with all fields filled', () => {
|
||||
const itemHash = {
|
||||
uuid: '1-2-3',
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.Note,
|
||||
duplicate_of: '222',
|
||||
auth_hash: 'aaa',
|
||||
deleted: true,
|
||||
enc_item_key: 'qweqwe1',
|
||||
items_key_id: 'asdasd1',
|
||||
created_at: timeHelper.formatDate(new Date(1616164633241), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'),
|
||||
updated_at: timeHelper.formatDate(new Date(1616164633242), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const item = createFactory().create({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
|
||||
|
||||
expect(item).toEqual({
|
||||
content: 'asdqwe1',
|
||||
contentSize: 341,
|
||||
contentType: 'Note',
|
||||
createdAt: expect.any(Date),
|
||||
updatedWithSession: '1-2-3',
|
||||
createdAtTimestamp: 1616164633241000,
|
||||
encItemKey: 'qweqwe1',
|
||||
itemsKeyId: 'asdasd1',
|
||||
authHash: 'aaa',
|
||||
deleted: true,
|
||||
duplicateOf: '222',
|
||||
updatedAt: expect.any(Date),
|
||||
updatedAtTimestamp: 1616164633241568,
|
||||
userUuid: 'a-b-c',
|
||||
uuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create an item based on item hash with created at timestamp', () => {
|
||||
const itemHash = {
|
||||
uuid: '1-2-3',
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe1',
|
||||
items_key_id: 'asdasd1',
|
||||
created_at_timestamp: 1616164633241312,
|
||||
updated_at: timeHelper.formatDate(new Date(1616164633242), 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const item = createFactory().create({ userUuid: 'a-b-c', itemHash, sessionUuid: '1-2-3' })
|
||||
|
||||
expect(item).toEqual({
|
||||
content: 'asdqwe1',
|
||||
contentSize: 341,
|
||||
contentType: 'Note',
|
||||
createdAt: expect.any(Date),
|
||||
updatedWithSession: '1-2-3',
|
||||
createdAtTimestamp: 1616164633241312,
|
||||
encItemKey: 'qweqwe1',
|
||||
itemsKeyId: 'asdasd1',
|
||||
updatedAt: expect.any(Date),
|
||||
updatedAtTimestamp: 1616164633241568,
|
||||
userUuid: 'a-b-c',
|
||||
uuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,79 +0,0 @@
|
|||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { ItemProjection } from '../../Projection/ItemProjection'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { Item } from './Item'
|
||||
import { ItemFactoryInterface } from './ItemFactoryInterface'
|
||||
import { ItemHash } from './ItemHash'
|
||||
|
||||
export class ItemFactory implements ItemFactoryInterface {
|
||||
constructor(private timer: TimerInterface, private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
|
||||
|
||||
createStub(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item {
|
||||
const item = this.create(dto)
|
||||
|
||||
if (dto.itemHash.content === undefined) {
|
||||
item.content = null
|
||||
}
|
||||
|
||||
if (dto.itemHash.updated_at_timestamp) {
|
||||
item.updatedAtTimestamp = dto.itemHash.updated_at_timestamp
|
||||
item.updatedAt = this.timer.convertMicrosecondsToDate(dto.itemHash.updated_at_timestamp)
|
||||
} else if (dto.itemHash.updated_at) {
|
||||
item.updatedAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.updated_at)
|
||||
item.updatedAt = this.timer.convertStringDateToDate(dto.itemHash.updated_at)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
create(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item {
|
||||
const newItem = new Item()
|
||||
newItem.uuid = dto.itemHash.uuid
|
||||
newItem.updatedWithSession = dto.sessionUuid
|
||||
newItem.contentSize = 0
|
||||
if (dto.itemHash.content) {
|
||||
newItem.content = dto.itemHash.content
|
||||
}
|
||||
newItem.userUuid = dto.userUuid
|
||||
if (dto.itemHash.content_type) {
|
||||
newItem.contentType = dto.itemHash.content_type
|
||||
}
|
||||
if (dto.itemHash.enc_item_key) {
|
||||
newItem.encItemKey = dto.itemHash.enc_item_key
|
||||
}
|
||||
if (dto.itemHash.items_key_id) {
|
||||
newItem.itemsKeyId = dto.itemHash.items_key_id
|
||||
}
|
||||
if (dto.itemHash.duplicate_of) {
|
||||
newItem.duplicateOf = dto.itemHash.duplicate_of
|
||||
}
|
||||
if (dto.itemHash.deleted !== undefined) {
|
||||
newItem.deleted = dto.itemHash.deleted
|
||||
}
|
||||
if (dto.itemHash.auth_hash) {
|
||||
newItem.authHash = dto.itemHash.auth_hash
|
||||
}
|
||||
|
||||
const now = this.timer.getTimestampInMicroseconds()
|
||||
const nowDate = this.timer.convertMicrosecondsToDate(now)
|
||||
|
||||
newItem.updatedAtTimestamp = now
|
||||
newItem.updatedAt = nowDate
|
||||
|
||||
newItem.createdAtTimestamp = now
|
||||
newItem.createdAt = nowDate
|
||||
|
||||
if (dto.itemHash.created_at_timestamp) {
|
||||
newItem.createdAtTimestamp = dto.itemHash.created_at_timestamp
|
||||
newItem.createdAt = this.timer.convertMicrosecondsToDate(dto.itemHash.created_at_timestamp)
|
||||
} else if (dto.itemHash.created_at) {
|
||||
newItem.createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
|
||||
newItem.createdAt = this.timer.convertStringDateToDate(dto.itemHash.created_at)
|
||||
}
|
||||
|
||||
newItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(newItem)))
|
||||
|
||||
return newItem
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { Item } from './Item'
|
||||
import { ItemHash } from './ItemHash'
|
||||
|
||||
export interface ItemFactoryInterface {
|
||||
create(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item
|
||||
createStub(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Item
|
||||
}
|
|
@ -1,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
|
||||
|
|
16
packages/syncing-server/src/Domain/Item/ItemProps.ts
Normal file
16
packages/syncing-server/src/Domain/Item/ItemProps.ts
Normal file
|
@ -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
|
||||
}
|
|
@ -16,8 +16,8 @@ export interface ItemRepositoryInterface {
|
|||
findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]>
|
||||
findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null>
|
||||
findByUuid(uuid: string): Promise<Item | null>
|
||||
remove(item: Item): Promise<Item>
|
||||
save(item: Item): Promise<Item>
|
||||
remove(item: Item): Promise<void>
|
||||
save(item: Item): Promise<void>
|
||||
markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void>
|
||||
updateContentSize(itemUuid: string, contentSize: number): Promise<void>
|
||||
}
|
||||
|
|
|
@ -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<Item, ItemProjection>
|
||||
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<Item>
|
||||
item2 = {
|
||||
uuid: '2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
createdAt: new Date(1616164633241312),
|
||||
createdAtTimestamp: 1616164633241312,
|
||||
updatedAt: new Date(1616164633241312),
|
||||
updatedAtTimestamp: 1616164633241312,
|
||||
} as jest.Mocked<Item>
|
||||
item1 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
item2 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
itemHash1 = {
|
||||
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<ItemHash>
|
||||
|
@ -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<ItemHash>
|
||||
|
||||
emptyHash = {
|
||||
uuid: '2-3-4',
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockReturnValue([item1.uuid, item2.uuid])
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest
|
||||
.fn()
|
||||
.mockReturnValue([item1.id.toString(), item2.id.toString()])
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item1, item2])
|
||||
itemRepository.countAll = jest.fn().mockReturnValue(2)
|
||||
itemRepository.save = jest.fn().mockImplementation((item: Item) => item)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
|
||||
|
@ -136,13 +140,6 @@ describe('ItemService', () => {
|
|||
.fn()
|
||||
.mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds))
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createDuplicateItemSyncedEvent = jest.fn()
|
||||
domainEventFactory.createItemRevisionCreationRequested = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
|
@ -152,31 +149,28 @@ describe('ItemService', () => {
|
|||
itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
|
||||
itemSaveValidator.validate = jest.fn().mockReturnValue({ passed: true })
|
||||
|
||||
newItem = {
|
||||
contentType: ContentType.Note,
|
||||
} as jest.Mocked<Item>
|
||||
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<ItemFactoryInterface>
|
||||
itemFactory.create = jest.fn().mockReturnValue(newItem)
|
||||
itemFactory.createStub = jest.fn().mockReturnValue(newItem)
|
||||
saveNewItemUseCase = {} as jest.Mocked<SaveNewItem>
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(newItem))
|
||||
|
||||
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
|
||||
itemProjector.projectFull = jest.fn().mockReturnValue({
|
||||
uuid: '1-2-3',
|
||||
items_key_id: 'foobar',
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'foobar',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed viverra tellus in hac habitasse. Tortor posuere ac ut consequat semper. Ut diam quam nulla porttitor. Sapien pellentesque habitant morbi tristique senectus et netus et malesuada. Dapibus ultrices in iaculis nunc. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Faucibus et molestie ac feugiat sed lectus vestibulum mattis. Eu consequat ac felis donec. Eget velit aliquet sagittis id. Nullam eget felis eget nunc. Turpis in eu mi bibendum neque egestas congue.',
|
||||
content_type: ContentType.Note,
|
||||
auth_hash: 'foobar',
|
||||
deleted: false,
|
||||
created_at: '2022-09-01 10:00:00',
|
||||
created_at_timestamp: 123123123123123,
|
||||
updated_at: '2022-09-01 10:00:00',
|
||||
updated_at_timestamp: 123123123123123,
|
||||
updated_with_session: '2-4-5',
|
||||
})
|
||||
updateExistingItemUseCase = {} as jest.Mocked<UpdateExistingItem>
|
||||
updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(item1))
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from last sync with sync token version 1', async () => {
|
||||
|
@ -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<Item>
|
||||
const item4 = {
|
||||
uuid: '4-5-6',
|
||||
} as jest.Mocked<Item>
|
||||
const item3 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue()
|
||||
const item4 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000004'),
|
||||
).getValue()
|
||||
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item3, item4])
|
||||
|
||||
|
@ -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<Item>
|
||||
itemFactory.create = jest.fn().mockReturnValueOnce(duplicateItem)
|
||||
const duplicateItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241570), new Date(1616164633241570)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241570, 1616164633241570).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000005'),
|
||||
).getValue()
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(duplicateItem))
|
||||
|
||||
const result = await createService().saveItems({
|
||||
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<Item>
|
||||
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<Item>)
|
||||
.mockReturnValueOnce({ updatedAtTimestamp: item2Timestamp, duplicateOf: null } as jest.Mocked<Item>)
|
||||
.mockReturnValueOnce({ updatedAtTimestamp: item3Timestamp, duplicateOf: null } as jest.Mocked<Item>)
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item1.props,
|
||||
timestamps: Timestamps.create(item1Timestamp, item1Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item2.props,
|
||||
timestamps: Timestamps.create(item2Timestamp, item2Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item2.props,
|
||||
timestamps: Timestamps.create(item3Timestamp, item3Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
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({
|
||||
|
|
|
@ -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<Item, ItemProjection>,
|
||||
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<Item>): Promise<Array<Item>> {
|
||||
const itemsKeys = await this.itemRepository.findAll({
|
||||
userUuid,
|
||||
contentType: ContentType.ItemsKey,
|
||||
contentType: ContentType.TYPES.ItemsKey,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
|
||||
const retrievedItemsIds: Array<string> = retrievedItems.map((item: Item) => item.uuid)
|
||||
const retrievedItemsIds: Array<string> = retrievedItems.map((item: Item) => item.id.toString())
|
||||
|
||||
itemsKeys.forEach((itemKey: Item) => {
|
||||
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<Item>): 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<Item> {
|
||||
dto.existingItem.updatedWithSession = dto.sessionUuid
|
||||
dto.existingItem.contentSize = 0
|
||||
if (dto.itemHash.content) {
|
||||
dto.existingItem.content = dto.itemHash.content
|
||||
}
|
||||
if (dto.itemHash.content_type) {
|
||||
dto.existingItem.contentType = dto.itemHash.content_type
|
||||
}
|
||||
if (dto.itemHash.deleted !== undefined) {
|
||||
dto.existingItem.deleted = dto.itemHash.deleted
|
||||
}
|
||||
let wasMarkedAsDuplicate = false
|
||||
if (dto.itemHash.duplicate_of) {
|
||||
wasMarkedAsDuplicate = !dto.existingItem.duplicateOf
|
||||
dto.existingItem.duplicateOf = dto.itemHash.duplicate_of
|
||||
}
|
||||
if (dto.itemHash.auth_hash) {
|
||||
dto.existingItem.authHash = dto.itemHash.auth_hash
|
||||
}
|
||||
if (dto.itemHash.enc_item_key) {
|
||||
dto.existingItem.encItemKey = dto.itemHash.enc_item_key
|
||||
}
|
||||
if (dto.itemHash.items_key_id) {
|
||||
dto.existingItem.itemsKeyId = dto.itemHash.items_key_id
|
||||
}
|
||||
|
||||
const updatedAt = this.timer.getTimestampInMicroseconds()
|
||||
const secondsFromLastUpdate = this.timer.convertMicrosecondsToSeconds(
|
||||
updatedAt - dto.existingItem.updatedAtTimestamp,
|
||||
)
|
||||
|
||||
if (dto.itemHash.created_at_timestamp) {
|
||||
dto.existingItem.createdAtTimestamp = dto.itemHash.created_at_timestamp
|
||||
dto.existingItem.createdAt = this.timer.convertMicrosecondsToDate(dto.itemHash.created_at_timestamp)
|
||||
} else if (dto.itemHash.created_at) {
|
||||
dto.existingItem.createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
|
||||
dto.existingItem.createdAt = this.timer.convertStringDateToDate(dto.itemHash.created_at)
|
||||
}
|
||||
|
||||
dto.existingItem.updatedAtTimestamp = updatedAt
|
||||
dto.existingItem.updatedAt = this.timer.convertMicrosecondsToDate(updatedAt)
|
||||
|
||||
dto.existingItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(dto.existingItem)))
|
||||
|
||||
if (dto.itemHash.deleted === true) {
|
||||
dto.existingItem.deleted = true
|
||||
dto.existingItem.content = null
|
||||
dto.existingItem.contentSize = 0
|
||||
dto.existingItem.encItemKey = null
|
||||
dto.existingItem.authHash = null
|
||||
dto.existingItem.itemsKeyId = null
|
||||
}
|
||||
|
||||
const savedItem = await this.itemRepository.save(dto.existingItem)
|
||||
|
||||
if (secondsFromLastUpdate >= this.revisionFrequency) {
|
||||
if ([ContentType.Note, ContentType.File].includes(savedItem.contentType as ContentType)) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemRevisionCreationRequested(savedItem.uuid, savedItem.userUuid),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (wasMarkedAsDuplicate) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createDuplicateItemSyncedEvent(savedItem.uuid, savedItem.userUuid),
|
||||
)
|
||||
}
|
||||
|
||||
return savedItem
|
||||
}
|
||||
|
||||
private async saveNewItem(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: string | null }): Promise<Item> {
|
||||
const newItem = this.itemFactory.create(dto)
|
||||
|
||||
const savedItem = await this.itemRepository.save(newItem)
|
||||
|
||||
if ([ContentType.Note, ContentType.File].includes(savedItem.contentType as ContentType)) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemRevisionCreationRequested(savedItem.uuid, savedItem.userUuid),
|
||||
)
|
||||
}
|
||||
|
||||
if (savedItem.duplicateOf) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createDuplicateItemSyncedEvent(savedItem.uuid, savedItem.userUuid),
|
||||
)
|
||||
}
|
||||
|
||||
return savedItem
|
||||
}
|
||||
|
||||
private getLastSyncTime(dto: GetItemsDTO): number | undefined {
|
||||
let token = dto.syncToken
|
||||
if (dto.cursorToken !== undefined && dto.cursorToken !== null) {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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<ItemSaveRuleResult> {
|
||||
const validContentType = Object.values(ContentType).includes(dto.itemHash.content_type as ContentType)
|
||||
|
||||
if (!validContentType) {
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return {
|
||||
passed: false,
|
||||
conflict: {
|
||||
|
|
|
@ -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<Item>
|
||||
existingItem.userUuid = '2-3-4'
|
||||
existingItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
})
|
||||
|
||||
it('should filter out items belonging to a different user', async () => {
|
||||
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',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<ItemSaveRuleResult> {
|
||||
const itemBelongsToADifferentUser = dto.existingItem !== null && dto.existingItem.userUuid !== dto.userUuid
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return {
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: dto.itemHash,
|
||||
type: ConflictType.UuidError,
|
||||
},
|
||||
}
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const itemBelongsToADifferentUser = dto.existingItem !== null && !dto.existingItem.props.userUuid.equals(userUuid)
|
||||
if (itemBelongsToADifferentUser) {
|
||||
return {
|
||||
passed: false,
|
||||
|
|
|
@ -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<Item>
|
||||
existingItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash = {
|
||||
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<ItemHash>
|
||||
|
@ -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]',
|
||||
)
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
import 'reflect-metadata'
|
||||
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
import { Item } from '../Item'
|
||||
|
||||
import { UuidFilter } from './UuidFilter'
|
||||
|
||||
describe('UuidFilter', () => {
|
||||
const createFilter = () => new UuidFilter()
|
||||
|
||||
it('should filter out items with invalid uuid', async () => {
|
||||
const invalidUuids = [
|
||||
'c73bcdcc-2669-4bf6-81d3-e4an73fb11fd',
|
||||
'c73bcdcc26694bf681d3e4ae73fb11fd',
|
||||
'definitely-not-a-uuid',
|
||||
'1-2-3',
|
||||
'test',
|
||||
"(select load_file('\\\\\\\\iugt7mazsk477",
|
||||
'/etc/passwd',
|
||||
"eval(compile('for x in range(1):\\n i",
|
||||
]
|
||||
|
||||
for (const invalidUuid of invalidUuids) {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: invalidUuid,
|
||||
content_type: ContentType.Note,
|
||||
},
|
||||
existingItem: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: invalidUuid,
|
||||
content_type: ContentType.Note,
|
||||
},
|
||||
type: 'uuid_error',
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should leave items with valid uuid', async () => {
|
||||
const validUuids = [
|
||||
'123e4567-e89b-12d3-a456-426655440000',
|
||||
'c73bcdcc-2669-4bf6-81d3-e4ae73fb11fd',
|
||||
'C73BCDCC-2669-4Bf6-81d3-E4AE73FB11FD',
|
||||
]
|
||||
|
||||
for (const validUuid of validUuids) {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: validUuid,
|
||||
content_type: ContentType.Note,
|
||||
},
|
||||
existingItem: {} as jest.Mocked<Item>,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
|
@ -1,25 +0,0 @@
|
|||
import { validate } from 'uuid'
|
||||
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
|
||||
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
|
||||
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
|
||||
export class UuidFilter implements ItemSaveRuleInterface {
|
||||
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
|
||||
const validUuid = validate(dto.itemHash.uuid)
|
||||
|
||||
if (!validUuid) {
|
||||
return {
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: dto.itemHash,
|
||||
type: ConflictType.UuidError,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passed: true,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ItemProjection>
|
||||
saved_items: Array<ItemProjection>
|
||||
retrieved_items: Array<ItemHttpRepresentation>
|
||||
saved_items: Array<ItemHttpRepresentation>
|
||||
unsaved: Array<{
|
||||
item: ItemProjection | ItemHash
|
||||
item: ItemHttpRepresentation | ItemHash
|
||||
error: {
|
||||
tag: ConflictType
|
||||
}
|
||||
|
|
|
@ -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<ItemProjection>
|
||||
saved_items: Array<SavedItemProjection>
|
||||
conflicts: Array<ItemConflictProjection>
|
||||
retrieved_items: Array<ItemHttpRepresentation>
|
||||
saved_items: Array<SavedItemHttpRepresentation>
|
||||
conflicts: Array<ItemConflictHttpRepresentation>
|
||||
sync_token: string
|
||||
cursor_token?: string
|
||||
}
|
||||
|
|
|
@ -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<Item, ItemProjection>
|
||||
let item1Projection: ItemProjection
|
||||
let item2Projection: ItemProjection
|
||||
let itemProjector: MapperInterface<Item, ItemHttpRepresentation>
|
||||
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<ItemProjection>
|
||||
} as jest.Mocked<ItemHttpRepresentation>
|
||||
item2Projection = {
|
||||
uuid: '2-3-4',
|
||||
} as jest.Mocked<ItemProjection>
|
||||
} as jest.Mocked<ItemHttpRepresentation>
|
||||
|
||||
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
|
||||
itemProjector.projectFull = jest.fn().mockImplementation((item: Item) => {
|
||||
if (item.uuid === '1-2-3') {
|
||||
itemProjector = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
|
||||
itemProjector.toProjection = jest.fn().mockImplementation((item: Item) => {
|
||||
if (item.id.toString() === '00000000-0000-0000-0000-000000000000') {
|
||||
return item1Projection
|
||||
} 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<Item>
|
||||
item1 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
item2 = {
|
||||
uuid: '2-3-4',
|
||||
} as jest.Mocked<Item>
|
||||
item2 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
})
|
||||
|
||||
it('should turn sync items response into a sync response for API Version 20161215', async () => {
|
||||
|
@ -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<ItemHash>
|
||||
|
||||
const duplicateItem1 = Object.assign({}, item1)
|
||||
duplicateItem1.updatedAtTimestamp = item1.updatedAtTimestamp + 21_000_000
|
||||
const duplicateItem1 = Item.create(
|
||||
{
|
||||
...item1.props,
|
||||
timestamps: Timestamps.create(
|
||||
item1.props.timestamps.createdAt,
|
||||
item1.props.timestamps.updatedAt + 21_000_000,
|
||||
).getValue(),
|
||||
},
|
||||
item1.id,
|
||||
).getValue()
|
||||
|
||||
const duplicateItem2 = Object.assign({}, item2)
|
||||
const duplicateItem2 = Item.create({ ...item2.props }).getValue()
|
||||
|
||||
expect(
|
||||
await createFactory().createResponse({
|
||||
|
|
|
@ -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<Item, ItemProjection>) {}
|
||||
constructor(private mapper: MapperInterface<Item, ItemHttpRepresentation>) {}
|
||||
|
||||
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20161215> {
|
||||
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
|
||||
? <ItemProjection>await this.itemProjector.projectFull(conflict.serverItem)
|
||||
: <ItemHash>conflict.unsavedItem,
|
||||
item: conflict.serverItem ? this.mapper.toProjection(conflict.serverItem) : <ItemHash>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(<ItemProjection>await this.itemProjector.projectFull(item))
|
||||
retrievedItems.push(this.mapper.toProjection(item))
|
||||
}
|
||||
|
||||
const savedItems = []
|
||||
for (const item of syncItemsResponse.savedItems) {
|
||||
savedItems.push(<ItemProjection>await this.itemProjector.projectFull(item))
|
||||
savedItems.push(this.mapper.toProjection(item))
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -64,16 +62,16 @@ export class SyncResponseFactory20161215 implements SyncResponseFactoryInterface
|
|||
unsavedItems: Array<ItemConflict>
|
||||
retrievedItems: Array<Item>
|
||||
} {
|
||||
const savedIds: Array<string> = savedItems.map((savedItem: Item) => savedItem.uuid)
|
||||
const retrievedIds: Array<string> = retrievedItems.map((retrievedItem: Item) => retrievedItem.uuid)
|
||||
const savedIds: Array<string> = savedItems.map((savedItem: Item) => savedItem.id.toString())
|
||||
const retrievedIds: Array<string> = retrievedItems.map((retrievedItem: Item) => retrievedItem.id.toString())
|
||||
|
||||
const conflictingIds = savedIds.filter((savedId) => retrievedIds.includes(savedId))
|
||||
|
||||
for (const conflictingId of conflictingIds) {
|
||||
const savedItem = <Item>savedItems.find((item) => item.uuid === conflictingId)
|
||||
const conflictedItem = <Item>retrievedItems.find((item) => item.uuid === conflictingId)
|
||||
const savedItem = <Item>savedItems.find((item) => item.id.toString() === conflictingId)
|
||||
const conflictedItem = <Item>retrievedItems.find((item) => item.id.toString() === conflictingId)
|
||||
|
||||
const difference = savedItem.updatedAtTimestamp - conflictedItem.updatedAtTimestamp
|
||||
const difference = savedItem.props.timestamps.updatedAt - conflictedItem.props.timestamps.updatedAt
|
||||
|
||||
if (Math.abs(difference) > this.LEGACY_MIN_CONFLICT_INTERVAL) {
|
||||
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 {
|
||||
|
|
|
@ -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<Item, ItemProjection>
|
||||
let savedItemProjector: ProjectorInterface<Item, SavedItemProjection>
|
||||
let itemConflictProjector: ProjectorInterface<ItemConflict, ItemConflictProjection>
|
||||
let itemProjection: ItemProjection
|
||||
let savedItemProjection: SavedItemProjection
|
||||
let itemConflictProjection: ItemConflictProjection
|
||||
let itemMapper: MapperInterface<Item, ItemHttpRepresentation>
|
||||
let savedItemMapper: MapperInterface<Item, SavedItemHttpRepresentation>
|
||||
let itemConflictMapper: MapperInterface<ItemConflict, ItemConflictHttpRepresentation>
|
||||
let itemProjection: ItemHttpRepresentation
|
||||
let savedItemHttpRepresentation: SavedItemHttpRepresentation
|
||||
let itemConflictProjection: ItemConflictHttpRepresentation
|
||||
let item1: Item
|
||||
let 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<ItemProjection>
|
||||
} as jest.Mocked<ItemHttpRepresentation>
|
||||
|
||||
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
|
||||
itemProjector.projectFull = jest.fn().mockReturnValue(itemProjection)
|
||||
itemMapper = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
|
||||
itemMapper.toProjection = jest.fn().mockReturnValue(itemProjection)
|
||||
|
||||
itemConflictProjector = {} as jest.Mocked<ProjectorInterface<ItemConflict, ItemConflictProjection>>
|
||||
itemConflictProjector.projectFull = jest.fn().mockReturnValue(itemConflictProjection)
|
||||
itemConflictMapper = {} as jest.Mocked<MapperInterface<ItemConflict, ItemConflictHttpRepresentation>>
|
||||
itemConflictMapper.toProjection = jest.fn().mockReturnValue(itemConflictProjection)
|
||||
|
||||
savedItemProjection = {
|
||||
savedItemHttpRepresentation = {
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<SavedItemProjection>
|
||||
} as jest.Mocked<SavedItemHttpRepresentation>
|
||||
|
||||
savedItemProjector = {} as jest.Mocked<ProjectorInterface<Item, SavedItemProjection>>
|
||||
savedItemProjector.projectFull = jest.fn().mockReturnValue(savedItemProjection)
|
||||
savedItemMapper = {} as jest.Mocked<MapperInterface<Item, SavedItemHttpRepresentation>>
|
||||
savedItemMapper.toProjection = jest.fn().mockReturnValue(savedItemHttpRepresentation)
|
||||
|
||||
item1 = {} as jest.Mocked<Item>
|
||||
|
||||
|
@ -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',
|
||||
|
|
|
@ -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<Item, ItemProjection>,
|
||||
private itemConflictProjector: ProjectorInterface<ItemConflict, ItemConflictProjection>,
|
||||
private savedItemProjector: ProjectorInterface<Item, SavedItemProjection>,
|
||||
private httpMapper: MapperInterface<Item, ItemHttpRepresentation>,
|
||||
private itemConflictMapper: MapperInterface<ItemConflict, ItemConflictHttpRepresentation>,
|
||||
private savedItemMapper: MapperInterface<Item, SavedItemHttpRepresentation>,
|
||||
) {}
|
||||
|
||||
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {
|
||||
const retrievedItems = []
|
||||
for (const item of syncItemsResponse.retrievedItems) {
|
||||
retrievedItems.push(<ItemProjection>await this.itemProjector.projectFull(item))
|
||||
retrievedItems.push(this.httpMapper.toProjection(item))
|
||||
}
|
||||
|
||||
const savedItems = []
|
||||
for (const item of syncItemsResponse.savedItems) {
|
||||
savedItems.push(<SavedItemProjection>await this.savedItemProjector.projectFull(item))
|
||||
savedItems.push(this.savedItemMapper.toProjection(item))
|
||||
}
|
||||
|
||||
const conflicts = []
|
||||
for (const itemConflict of syncItemsResponse.conflicts) {
|
||||
conflicts.push(<ItemConflictProjection>await this.itemConflictProjector.projectFull(itemConflict))
|
||||
conflicts.push(this.itemConflictMapper.toProjection(itemConflict))
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
|
|
@ -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<IntegrityPayload[]> {
|
|||
) 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<IntegrityPayload[]> {
|
|||
) as number
|
||||
if (
|
||||
serverItemIntegrityPayloadUpdatedAtTimestamp !== clientItemIntegrityPayloadUpdatedAtTimestamp &&
|
||||
serverItemIntegrityPayload.content_type !== ContentType.ItemsKey
|
||||
serverItemIntegrityPayload.content_type !== ContentType.TYPES.ItemsKey
|
||||
) {
|
||||
mismatches.unshift({
|
||||
uuid: serverItemIntegrityPayloadUuid,
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { SaveNewItem } from './SaveNewItem'
|
||||
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { Item } from '../../../Item/Item'
|
||||
|
||||
describe('SaveNewItem', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let timer: TimerInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let itemHash1: ItemHash
|
||||
let item1: Item
|
||||
|
||||
const createUseCase = () => new SaveNewItem(itemRepository, timer, domainEventPublisher, domainEventFactory)
|
||||
|
||||
beforeEach(() => {
|
||||
const timeHelper = new Timer()
|
||||
|
||||
item1 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash1 = {
|
||||
uuid: '1-2-3',
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe1',
|
||||
items_key_id: 'asdasd1',
|
||||
created_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.save = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
|
||||
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertStringDateToDate = jest.fn().mockReturnValue(new Date(123456789))
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createDuplicateItemSyncedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
domainEventFactory.createItemRevisionCreationRequested = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
})
|
||||
|
||||
it('saves a new item', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(itemRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('saves a new empty item', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.content = undefined
|
||||
itemHash1.content_type = null
|
||||
itemHash1.enc_item_key = undefined
|
||||
itemHash1.items_key_id = undefined
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(itemRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('saves a new item with given timestamps', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.created_at_timestamp = 123
|
||||
itemHash1.updated_at_timestamp = 123
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('publishes a duplicate item synced event if the item is a duplicate', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.duplicate_of = '00000000-0000-0000-0000-000000000003'
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(domainEventFactory.createDuplicateItemSyncedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('publishes a item revision creation requested event if the item is a revision', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.updated_at = '2021-03-19T17:17:13.241Z'
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(domainEventFactory.createItemRevisionCreationRequested).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns a failure if the item cannot be saved', async () => {
|
||||
const mock = jest.spyOn(Item, 'create')
|
||||
mock.mockImplementation(() => {
|
||||
return Result.fail('Oops')
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('returns a failure if the user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-00000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns a failure if the session uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-00000000000',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns a failure if the content type is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.content_type = 'invalid'
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns a failure if the duplicate uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.duplicate_of = 'invalid'
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns a failure if it fails to create dates', async () => {
|
||||
const mock = jest.spyOn(Dates, 'create')
|
||||
mock.mockImplementation(() => {
|
||||
return Result.fail('Oops')
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('return a failure if it fails to create timestamps', async () => {
|
||||
const mock = jest.spyOn(Timestamps, 'create')
|
||||
mock.mockImplementation(() => {
|
||||
return Result.fail('Oops')
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
ContentType,
|
||||
Dates,
|
||||
Result,
|
||||
Timestamps,
|
||||
UniqueEntityId,
|
||||
UseCaseInterface,
|
||||
Uuid,
|
||||
} from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { SaveNewItemDTO } from './SaveNewItemDTO'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: SaveNewItemDTO): Promise<Result<Item>> {
|
||||
let updatedWithSession = null
|
||||
if (dto.sessionUuid) {
|
||||
const sessionUuidOrError = Uuid.create(dto.sessionUuid)
|
||||
if (sessionUuidOrError.isFailed()) {
|
||||
return Result.fail(sessionUuidOrError.getError())
|
||||
}
|
||||
updatedWithSession = sessionUuidOrError.getValue()
|
||||
}
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return Result.fail(contentTypeOrError.getError())
|
||||
}
|
||||
const contentType = contentTypeOrError.getValue()
|
||||
|
||||
let duplicateOf = null
|
||||
if (dto.itemHash.duplicate_of) {
|
||||
const duplicateOfOrError = Uuid.create(dto.itemHash.duplicate_of)
|
||||
if (duplicateOfOrError.isFailed()) {
|
||||
return Result.fail(duplicateOfOrError.getError())
|
||||
}
|
||||
duplicateOf = duplicateOfOrError.getValue()
|
||||
}
|
||||
|
||||
const now = this.timer.getTimestampInMicroseconds()
|
||||
const nowDate = this.timer.convertMicrosecondsToDate(now)
|
||||
|
||||
let createdAtDate = nowDate
|
||||
let createdAtTimestamp = now
|
||||
if (dto.itemHash.created_at_timestamp) {
|
||||
createdAtTimestamp = dto.itemHash.created_at_timestamp
|
||||
createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
|
||||
} else if (dto.itemHash.created_at) {
|
||||
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
|
||||
createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.created_at)
|
||||
}
|
||||
|
||||
const datesOrError = Dates.create(createdAtDate, nowDate)
|
||||
if (datesOrError.isFailed()) {
|
||||
return Result.fail(datesOrError.getError())
|
||||
}
|
||||
const dates = datesOrError.getValue()
|
||||
|
||||
const timestampsOrError = Timestamps.create(createdAtTimestamp, now)
|
||||
if (timestampsOrError.isFailed()) {
|
||||
return Result.fail(timestampsOrError.getError())
|
||||
}
|
||||
const timestamps = timestampsOrError.getValue()
|
||||
|
||||
const itemOrError = Item.create(
|
||||
{
|
||||
updatedWithSession,
|
||||
content: dto.itemHash.content ?? null,
|
||||
userUuid,
|
||||
contentType,
|
||||
encItemKey: dto.itemHash.enc_item_key ?? null,
|
||||
authHash: dto.itemHash.auth_hash ?? null,
|
||||
itemsKeyId: dto.itemHash.items_key_id ?? null,
|
||||
duplicateOf,
|
||||
deleted: dto.itemHash.deleted ?? false,
|
||||
dates,
|
||||
timestamps,
|
||||
},
|
||||
new UniqueEntityId(dto.itemHash.uuid),
|
||||
)
|
||||
if (itemOrError.isFailed()) {
|
||||
return Result.fail(itemOrError.getError())
|
||||
}
|
||||
const newItem = itemOrError.getValue()
|
||||
|
||||
await this.itemRepository.save(newItem)
|
||||
|
||||
if (contentType.value !== null && [ContentType.TYPES.Note, ContentType.TYPES.File].includes(contentType.value)) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemRevisionCreationRequested(
|
||||
newItem.id.toString(),
|
||||
newItem.props.userUuid.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (duplicateOf) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createDuplicateItemSyncedEvent(newItem.id.toString(), newItem.props.userUuid.value),
|
||||
)
|
||||
}
|
||||
|
||||
return Result.ok(newItem)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
|
||||
export interface SaveNewItemDTO {
|
||||
userUuid: string
|
||||
itemHash: ItemHash
|
||||
sessionUuid: string | null
|
||||
}
|
|
@ -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<Item>
|
||||
item2 = {
|
||||
uuid: '2-3-4',
|
||||
} as jest.Mocked<Item>
|
||||
item3 = {
|
||||
uuid: '3-4-5',
|
||||
} as jest.Mocked<Item>
|
||||
item1 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
item2 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
|
||||
).getValue()
|
||||
item3 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue()
|
||||
|
||||
itemHash = {
|
||||
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',
|
||||
|
|
|
@ -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<SyncItemsResponse> {
|
|||
const syncConflictIds: Array<string> = []
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { UpdateExistingItem } from './UpdateExistingItem'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('UpdateExistingItem', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let timer: TimerInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let itemHash1: ItemHash
|
||||
let item1: Item
|
||||
|
||||
const createUseCase = () => new UpdateExistingItem(itemRepository, timer, domainEventPublisher, domainEventFactory, 5)
|
||||
|
||||
beforeEach(() => {
|
||||
const timeHelper = new Timer()
|
||||
|
||||
item1 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash1 = {
|
||||
uuid: '1-2-3',
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe1',
|
||||
auth_hash: 'auth_hash',
|
||||
items_key_id: 'asdasd1',
|
||||
created_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.save = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
|
||||
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertStringDateToDate = jest.fn().mockReturnValue(new Date(123456789))
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createDuplicateItemSyncedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
domainEventFactory.createItemRevisionCreationRequested = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
})
|
||||
|
||||
it('should update item', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(itemRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return error if session uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: 'invalid-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if content type is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
content_type: 'invalid',
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should mark item as deleted if item hash is deleted', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
deleted: true,
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(itemRepository.save).toHaveBeenCalled()
|
||||
expect(item1.props.deleted).toBeTruthy()
|
||||
expect(item1.props.content).toBeNull()
|
||||
expect(item1.props.encItemKey).toBeNull()
|
||||
expect(item1.props.authHash).toBeNull()
|
||||
expect(item1.props.itemsKeyId).toBeNull()
|
||||
expect(item1.props.duplicateOf).toBeNull()
|
||||
})
|
||||
|
||||
it('should mark item as duplicate if item hash has duplicate_of', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
duplicate_of: '00000000-0000-0000-0000-000000000001',
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(itemRepository.save).toHaveBeenCalled()
|
||||
expect(item1.props.duplicateOf?.value).toBe('00000000-0000-0000-0000-000000000001')
|
||||
})
|
||||
|
||||
it('shuld return error if duplicate uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
duplicate_of: 'invalid-uuid',
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should update item with update timestamps', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
updated_at_timestamp: 123,
|
||||
created_at_timestamp: 123,
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(itemRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return error if created at time is not give in any form', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
created_at: undefined,
|
||||
created_at_timestamp: undefined,
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if dates could not be created from timestamps', async () => {
|
||||
const mock = jest.spyOn(Dates, 'create')
|
||||
mock.mockImplementation(() => {
|
||||
return Result.fail('Oops')
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('should return error if timestamps could not be created from timestamps', async () => {
|
||||
const mock = jest.spyOn(Timestamps, 'create')
|
||||
mock.mockImplementation(() => {
|
||||
return Result.fail('Oops')
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
},
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
mock.mockRestore()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,135 @@
|
|||
import { ContentType, Dates, Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { UpdateExistingItemDTO } from './UpdateExistingItemDTO'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private revisionFrequency: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: UpdateExistingItemDTO): Promise<Result<Item>> {
|
||||
let sessionUuid = null
|
||||
if (dto.sessionUuid) {
|
||||
const sessionUuidOrError = Uuid.create(dto.sessionUuid)
|
||||
if (sessionUuidOrError.isFailed()) {
|
||||
return Result.fail(sessionUuidOrError.getError())
|
||||
}
|
||||
sessionUuid = sessionUuidOrError.getValue()
|
||||
}
|
||||
dto.existingItem.props.updatedWithSession = sessionUuid
|
||||
|
||||
if (dto.itemHash.content) {
|
||||
dto.existingItem.props.content = dto.itemHash.content
|
||||
}
|
||||
|
||||
if (dto.itemHash.content_type) {
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return Result.fail(contentTypeOrError.getError())
|
||||
}
|
||||
const contentType = contentTypeOrError.getValue()
|
||||
dto.existingItem.props.contentType = contentType
|
||||
}
|
||||
|
||||
if (dto.itemHash.deleted !== undefined) {
|
||||
dto.existingItem.props.deleted = dto.itemHash.deleted
|
||||
}
|
||||
|
||||
let wasMarkedAsDuplicate = false
|
||||
if (dto.itemHash.duplicate_of) {
|
||||
const duplicateOfOrError = Uuid.create(dto.itemHash.duplicate_of)
|
||||
if (duplicateOfOrError.isFailed()) {
|
||||
return Result.fail(duplicateOfOrError.getError())
|
||||
}
|
||||
wasMarkedAsDuplicate = dto.existingItem.props.duplicateOf === null
|
||||
dto.existingItem.props.duplicateOf = duplicateOfOrError.getValue()
|
||||
}
|
||||
|
||||
if (dto.itemHash.auth_hash) {
|
||||
dto.existingItem.props.authHash = dto.itemHash.auth_hash
|
||||
}
|
||||
if (dto.itemHash.enc_item_key) {
|
||||
dto.existingItem.props.encItemKey = dto.itemHash.enc_item_key
|
||||
}
|
||||
if (dto.itemHash.items_key_id) {
|
||||
dto.existingItem.props.itemsKeyId = dto.itemHash.items_key_id
|
||||
}
|
||||
|
||||
const updatedAtTimestamp = this.timer.getTimestampInMicroseconds()
|
||||
const secondsFromLastUpdate = this.timer.convertMicrosecondsToSeconds(
|
||||
updatedAtTimestamp - dto.existingItem.props.timestamps.updatedAt,
|
||||
)
|
||||
const updatedAtDate = this.timer.convertMicrosecondsToDate(updatedAtTimestamp)
|
||||
|
||||
let createdAtTimestamp: number
|
||||
let createdAtDate: Date
|
||||
if (dto.itemHash.created_at_timestamp) {
|
||||
createdAtTimestamp = dto.itemHash.created_at_timestamp
|
||||
createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
|
||||
} else if (dto.itemHash.created_at) {
|
||||
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
|
||||
createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.created_at)
|
||||
} else {
|
||||
return Result.fail('Created at timestamp is required.')
|
||||
}
|
||||
|
||||
const datesOrError = Dates.create(createdAtDate, updatedAtDate)
|
||||
if (datesOrError.isFailed()) {
|
||||
return Result.fail(datesOrError.getError())
|
||||
}
|
||||
dto.existingItem.props.dates = datesOrError.getValue()
|
||||
|
||||
const timestampsOrError = Timestamps.create(createdAtTimestamp, updatedAtTimestamp)
|
||||
if (timestampsOrError.isFailed()) {
|
||||
return Result.fail(timestampsOrError.getError())
|
||||
}
|
||||
dto.existingItem.props.timestamps = timestampsOrError.getValue()
|
||||
|
||||
dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
|
||||
|
||||
if (dto.itemHash.deleted === true) {
|
||||
dto.existingItem.props.deleted = true
|
||||
dto.existingItem.props.content = null
|
||||
dto.existingItem.props.contentSize = 0
|
||||
dto.existingItem.props.encItemKey = null
|
||||
dto.existingItem.props.authHash = null
|
||||
dto.existingItem.props.itemsKeyId = null
|
||||
}
|
||||
|
||||
await this.itemRepository.save(dto.existingItem)
|
||||
|
||||
if (secondsFromLastUpdate >= this.revisionFrequency) {
|
||||
if (
|
||||
dto.existingItem.props.contentType.value !== null &&
|
||||
[ContentType.TYPES.Note, ContentType.TYPES.File].includes(dto.existingItem.props.contentType.value)
|
||||
) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemRevisionCreationRequested(
|
||||
dto.existingItem.id.toString(),
|
||||
dto.existingItem.props.userUuid.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (wasMarkedAsDuplicate) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createDuplicateItemSyncedEvent(
|
||||
dto.existingItem.id.toString(),
|
||||
dto.existingItem.props.userUuid.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return Result.ok(dto.existingItem)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Item } from '../../../Item/Item'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
|
||||
export interface UpdateExistingItemDTO {
|
||||
existingItem: Item
|
||||
itemHash: ItemHash
|
||||
sessionUuid: string | null
|
||||
}
|
|
@ -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<Item, ItemProjection>,
|
||||
private mapper: MapperInterface<Item, ItemBackupRepresentation>,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
|
@ -22,12 +22,12 @@ export class FSItemBackupService implements ItemBackupServiceInterface {
|
|||
|
||||
async dump(item: Item): Promise<string> {
|
||||
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 })
|
||||
|
||||
|
|
|
@ -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<Item, ItemProjection>,
|
||||
protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
|
||||
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()) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Item, ItemProjection>
|
||||
let mapper: MapperInterface<Item, ItemHttpRepresentation>
|
||||
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<ProjectorInterface<Item, ItemProjection>>
|
||||
itemProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
mapper = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
|
||||
mapper.toProjection = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
syncItems = {} as jest.Mocked<SyncItems>
|
||||
syncItems.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,
|
||||
|
|
|
@ -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<Item, ItemProjection>,
|
||||
@inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
|
||||
@inject(TYPES.Sync_SyncResponseFactoryResolver)
|
||||
override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
|
||||
) {
|
||||
super(syncItems, checkIntegrity, getItem, itemProjector, syncResponseFactoryResolver)
|
||||
super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver)
|
||||
}
|
||||
|
||||
@httpPost('/sync')
|
||||
|
|
|
@ -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<Item, ItemProjection>,
|
||||
private backupMapper: MapperInterface<Item, ItemBackupRepresentation>,
|
||||
private httpMapper: MapperInterface<Item, ItemHttpRepresentation>,
|
||||
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<ItemProjection> = []
|
||||
let itemProjections: Array<ItemHttpRepresentation> = []
|
||||
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<string> {
|
||||
private async createBackupFile(
|
||||
itemRepresentations: ItemHttpRepresentation[],
|
||||
authParams: KeyParamsData,
|
||||
): Promise<string> {
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
|
|
118
packages/syncing-server/src/Infra/TypeORM/TypeORMItem.ts
Normal file
118
packages/syncing-server/src/Infra/TypeORM/TypeORMItem.ts
Normal file
|
@ -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
|
||||
}
|
|
@ -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<Item>) {}
|
||||
constructor(private ormRepository: Repository<TypeORMItem>, private mapper: MapperInterface<Item, TypeORMItem>) {}
|
||||
|
||||
async save(item: Item): Promise<Item> {
|
||||
return this.ormRepository.save(item)
|
||||
async save(item: Item): Promise<void> {
|
||||
const persistence = this.mapper.toProjection(item)
|
||||
|
||||
await this.ormRepository.save(persistence)
|
||||
}
|
||||
|
||||
async remove(item: Item): Promise<Item> {
|
||||
return this.ormRepository.remove(item)
|
||||
async remove(item: Item): Promise<void> {
|
||||
await this.ormRepository.remove(this.mapper.toProjection(item))
|
||||
}
|
||||
|
||||
async updateContentSize(itemUuid: string, contentSize: number): Promise<void> {
|
||||
|
@ -51,12 +56,18 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
|
|||
}
|
||||
|
||||
async findByUuid(uuid: string): Promise<Item | null> {
|
||||
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<Array<{ updated_at_timestamp: number }>> {
|
||||
|
@ -84,17 +95,25 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
|
|||
}
|
||||
|
||||
async findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null> {
|
||||
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<Item[]> {
|
||||
return this.createFindAllQueryBuilder(query).getMany()
|
||||
const persistence = await this.createFindAllQueryBuilder(query).getMany()
|
||||
|
||||
return persistence.map((p) => this.mapper.toDomain(p))
|
||||
}
|
||||
|
||||
async findAllRaw<T>(query: ItemQuery): Promise<T[]> {
|
||||
|
@ -126,7 +145,7 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
|
|||
.execute()
|
||||
}
|
||||
|
||||
private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<Item> {
|
||||
private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<TypeORMItem> {
|
||||
const queryBuilder = this.ormRepository.createQueryBuilder('item')
|
||||
|
||||
if (query.sortBy !== undefined && query.sortOrder !== undefined) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemBackupRepresentation } from './ItemBackupRepresentation'
|
||||
|
||||
export class ItemBackupMapper implements MapperInterface<Item, ItemBackupRepresentation> {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
toDomain(_projection: ItemBackupRepresentation): Item {
|
||||
throw new Error('Mapping from http representation to domain is not implemented.')
|
||||
}
|
||||
|
||||
toProjection(domain: Item): ItemBackupRepresentation {
|
||||
return {
|
||||
uuid: domain.id.toString(),
|
||||
items_key_id: domain.props.itemsKeyId,
|
||||
duplicate_of: domain.props.duplicateOf ? domain.props.duplicateOf.value : null,
|
||||
enc_item_key: domain.props.encItemKey,
|
||||
content: domain.props.content,
|
||||
content_type: domain.props.contentType.value as string,
|
||||
auth_hash: domain.props.authHash,
|
||||
deleted: !!domain.props.deleted,
|
||||
created_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.createdAt),
|
||||
created_at_timestamp: domain.props.timestamps.createdAt,
|
||||
updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
|
||||
updated_at_timestamp: domain.props.timestamps.updatedAt,
|
||||
updated_with_session: domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null,
|
||||
user_uuid: domain.props.userUuid.value,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemConflictHttpRepresentation } from './ItemConflictHttpRepresentation'
|
||||
import { ItemConflict } from '../../Domain/Item/ItemConflict'
|
||||
import { ItemHttpRepresentation } from './ItemHttpRepresentation'
|
||||
|
||||
export class ItemConflictHttpMapper implements MapperInterface<ItemConflict, ItemConflictHttpRepresentation> {
|
||||
constructor(private mapper: MapperInterface<Item, ItemHttpRepresentation>) {}
|
||||
|
||||
toDomain(_projection: ItemConflictHttpRepresentation): ItemConflict {
|
||||
throw new Error('Mapping from http representation to domain is not implemented.')
|
||||
}
|
||||
|
||||
toProjection(domain: ItemConflict): ItemConflictHttpRepresentation {
|
||||
const representation: ItemConflictHttpRepresentation = {
|
||||
unsaved_item: domain.unsavedItem,
|
||||
type: domain.type,
|
||||
}
|
||||
|
||||
if (domain.serverItem) {
|
||||
representation.server_item = this.mapper.toProjection(domain.serverItem)
|
||||
}
|
||||
|
||||
return representation
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { ConflictType } from '@standardnotes/responses'
|
||||
|
||||
import { ItemHash } from '../../Domain/Item/ItemHash'
|
||||
import { ItemHttpRepresentation } from './ItemHttpRepresentation'
|
||||
|
||||
export interface ItemConflictHttpRepresentation {
|
||||
server_item?: ItemHttpRepresentation
|
||||
unsaved_item?: ItemHash
|
||||
type: ConflictType
|
||||
}
|
31
packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts
Normal file
31
packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemHttpRepresentation } from './ItemHttpRepresentation'
|
||||
|
||||
export class ItemHttpMapper implements MapperInterface<Item, ItemHttpRepresentation> {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
toDomain(_projection: ItemHttpRepresentation): Item {
|
||||
throw new Error('Mapping from http representation to domain is not implemented.')
|
||||
}
|
||||
|
||||
toProjection(domain: Item): ItemHttpRepresentation {
|
||||
return {
|
||||
uuid: domain.id.toString(),
|
||||
items_key_id: domain.props.itemsKeyId,
|
||||
duplicate_of: domain.props.duplicateOf ? domain.props.duplicateOf.value : null,
|
||||
enc_item_key: domain.props.encItemKey,
|
||||
content: domain.props.content,
|
||||
content_type: domain.props.contentType.value as string,
|
||||
auth_hash: domain.props.authHash,
|
||||
deleted: !!domain.props.deleted,
|
||||
created_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.createdAt),
|
||||
created_at_timestamp: domain.props.timestamps.createdAt,
|
||||
updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
|
||||
updated_at_timestamp: domain.props.timestamps.updatedAt,
|
||||
updated_with_session: domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export type ItemProjection = {
|
||||
export interface ItemHttpRepresentation {
|
||||
uuid: string
|
||||
items_key_id: string | null
|
||||
duplicate_of: string | null
|
|
@ -0,0 +1,27 @@
|
|||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { SavedItemHttpRepresentation } from './SavedItemHttpRepresentation'
|
||||
|
||||
export class SavedItemHttpMapper implements MapperInterface<Item, SavedItemHttpRepresentation> {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
toDomain(_projection: SavedItemHttpRepresentation): Item {
|
||||
throw new Error('Mapping from http representation to domain is not implemented.')
|
||||
}
|
||||
|
||||
toProjection(domain: Item): SavedItemHttpRepresentation {
|
||||
return {
|
||||
uuid: domain.id.toString(),
|
||||
duplicate_of: domain.props.duplicateOf ? domain.props.duplicateOf.value : null,
|
||||
content_type: domain.props.contentType.value as string,
|
||||
auth_hash: domain.props.authHash,
|
||||
deleted: !!domain.props.deleted,
|
||||
created_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.createdAt),
|
||||
created_at_timestamp: domain.props.timestamps.createdAt,
|
||||
updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
|
||||
updated_at_timestamp: domain.props.timestamps.updatedAt,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export type SavedItemProjection = {
|
||||
export interface SavedItemHttpRepresentation {
|
||||
uuid: string
|
||||
duplicate_of: string | null
|
||||
content_type: string
|
|
@ -0,0 +1,96 @@
|
|||
import { Timestamps, MapperInterface, UniqueEntityId, Uuid, ContentType, Dates } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
|
||||
import { TypeORMItem } from '../../Infra/TypeORM/TypeORMItem'
|
||||
|
||||
export class ItemPersistenceMapper implements MapperInterface<Item, TypeORMItem> {
|
||||
toDomain(projection: TypeORMItem): Item {
|
||||
let duplicateOf = null
|
||||
if (projection.duplicateOf) {
|
||||
const duplicateOfOrError = Uuid.create(projection.duplicateOf)
|
||||
if (duplicateOfOrError.isFailed()) {
|
||||
throw new Error(`Failed to create item from projection: ${duplicateOfOrError.getError()}`)
|
||||
}
|
||||
duplicateOf = duplicateOfOrError.getValue()
|
||||
}
|
||||
|
||||
const contentTypeOrError = ContentType.create(projection.contentType)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
throw new Error(`Failed to create item from projection: ${contentTypeOrError.getError()}`)
|
||||
}
|
||||
const contentType = contentTypeOrError.getValue()
|
||||
|
||||
const userUuidOrError = Uuid.create(projection.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
throw new Error(`Failed to create item from projection: ${userUuidOrError.getError()}`)
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const datesOrError = Dates.create(projection.createdAt, projection.updatedAt)
|
||||
if (datesOrError.isFailed()) {
|
||||
throw new Error(`Failed to create item from projection: ${datesOrError.getError()}`)
|
||||
}
|
||||
const dates = datesOrError.getValue()
|
||||
|
||||
const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
|
||||
if (timestampsOrError.isFailed()) {
|
||||
throw new Error(`Failed to create item from projection: ${timestampsOrError.getError()}`)
|
||||
}
|
||||
const timestamps = timestampsOrError.getValue()
|
||||
|
||||
let updatedWithSession = null
|
||||
if (projection.updatedWithSession) {
|
||||
const updatedWithSessionOrError = Uuid.create(projection.updatedWithSession)
|
||||
if (updatedWithSessionOrError.isFailed()) {
|
||||
throw new Error(`Failed to create item from projection: ${updatedWithSessionOrError.getError()}`)
|
||||
}
|
||||
updatedWithSession = updatedWithSessionOrError.getValue()
|
||||
}
|
||||
|
||||
const itemOrError = Item.create(
|
||||
{
|
||||
duplicateOf,
|
||||
itemsKeyId: projection.itemsKeyId,
|
||||
content: projection.content,
|
||||
contentType,
|
||||
contentSize: projection.contentSize ?? undefined,
|
||||
encItemKey: projection.encItemKey,
|
||||
authHash: projection.authHash,
|
||||
userUuid,
|
||||
deleted: projection.deleted,
|
||||
dates,
|
||||
timestamps,
|
||||
updatedWithSession,
|
||||
},
|
||||
new UniqueEntityId(projection.uuid),
|
||||
)
|
||||
if (itemOrError.isFailed()) {
|
||||
throw new Error(`Failed to create item from projection: ${itemOrError.getError()}`)
|
||||
}
|
||||
|
||||
return itemOrError.getValue()
|
||||
}
|
||||
|
||||
toProjection(domain: Item): TypeORMItem {
|
||||
const typeorm = new TypeORMItem()
|
||||
|
||||
typeorm.uuid = domain.id.toString()
|
||||
typeorm.duplicateOf = domain.props.duplicateOf ? domain.props.duplicateOf.value : null
|
||||
typeorm.itemsKeyId = domain.props.itemsKeyId
|
||||
typeorm.content = domain.props.content
|
||||
typeorm.contentType = domain.props.contentType.value
|
||||
typeorm.contentSize = domain.props.contentSize ?? null
|
||||
typeorm.encItemKey = domain.props.encItemKey
|
||||
typeorm.authHash = domain.props.authHash
|
||||
typeorm.userUuid = domain.props.userUuid.value
|
||||
typeorm.deleted = domain.props.deleted
|
||||
typeorm.createdAt = domain.props.dates.createdAt
|
||||
typeorm.updatedAt = domain.props.dates.updatedAt
|
||||
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
|
||||
typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
|
||||
typeorm.updatedWithSession = domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null
|
||||
|
||||
return typeorm
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import 'reflect-metadata'
|
||||
|
||||
import { ProjectorInterface } from './ProjectorInterface'
|
||||
import { Item } from '../Domain/Item/Item'
|
||||
import { ItemConflict } from '../Domain/Item/ItemConflict'
|
||||
import { ItemConflictProjector } from './ItemConflictProjector'
|
||||
import { ItemHash } from '../Domain/Item/ItemHash'
|
||||
import { ItemProjection } from './ItemProjection'
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
|
||||
describe('ItemConflictProjector', () => {
|
||||
let itemProjector: ProjectorInterface<Item, ItemProjection>
|
||||
let itemProjection: ItemProjection
|
||||
let itemConflict1: ItemConflict
|
||||
let itemConflict2: ItemConflict
|
||||
let item: Item
|
||||
let itemHash: ItemHash
|
||||
|
||||
const createProjector = () => new ItemConflictProjector(itemProjector)
|
||||
|
||||
beforeEach(() => {
|
||||
itemProjection = {} as jest.Mocked<ItemProjection>
|
||||
|
||||
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
|
||||
itemProjector.projectFull = jest.fn().mockReturnValue(itemProjection)
|
||||
|
||||
item = {} as jest.Mocked<Item>
|
||||
|
||||
itemHash = {} as jest.Mocked<ItemHash>
|
||||
|
||||
itemConflict1 = {
|
||||
serverItem: item,
|
||||
type: ConflictType.ConflictingData,
|
||||
}
|
||||
|
||||
itemConflict2 = {
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
}
|
||||
})
|
||||
|
||||
it('should create a full projection of a server item conflict', async () => {
|
||||
expect(await createProjector().projectFull(itemConflict1)).toMatchObject({
|
||||
server_item: itemProjection,
|
||||
type: ConflictType.ConflictingData,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a full projection of an unsaved item conflict', async () => {
|
||||
expect(await createProjector().projectFull(itemConflict2)).toMatchObject({
|
||||
unsaved_item: itemHash,
|
||||
type: 'uuid_conflict',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error on custom projection', async () => {
|
||||
let error = null
|
||||
try {
|
||||
await createProjector().projectCustom('test', itemConflict1)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect((error as Error).message).toEqual('not implemented')
|
||||
})
|
||||
|
||||
it('should throw error on simple projection', async () => {
|
||||
let error = null
|
||||
try {
|
||||
await createProjector().projectSimple(itemConflict1)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect((error as Error).message).toEqual('not implemented')
|
||||
})
|
||||
})
|
|
@ -1,31 +0,0 @@
|
|||
import { ProjectorInterface } from './ProjectorInterface'
|
||||
|
||||
import { Item } from '../Domain/Item/Item'
|
||||
import { ItemConflict } from '../Domain/Item/ItemConflict'
|
||||
import { ItemConflictProjection } from './ItemConflictProjection'
|
||||
import { ItemProjection } from './ItemProjection'
|
||||
|
||||
export class ItemConflictProjector implements ProjectorInterface<ItemConflict, ItemConflictProjection> {
|
||||
constructor(private itemProjector: ProjectorInterface<Item, ItemProjection>) {}
|
||||
|
||||
async projectSimple(_itemConflict: ItemConflict): Promise<ItemConflictProjection> {
|
||||
throw Error('not implemented')
|
||||
}
|
||||
|
||||
async projectCustom(_projectionType: string, _itemConflict: ItemConflict): Promise<ItemConflictProjection> {
|
||||
throw Error('not implemented')
|
||||
}
|
||||
|
||||
async projectFull(itemConflict: ItemConflict): Promise<ItemConflictProjection> {
|
||||
const projection: ItemConflictProjection = {
|
||||
unsaved_item: itemConflict.unsavedItem,
|
||||
type: itemConflict.type,
|
||||
}
|
||||
|
||||
if (itemConflict.serverItem) {
|
||||
projection.server_item = <ItemProjection>await this.itemProjector.projectFull(itemConflict.serverItem)
|
||||
}
|
||||
|
||||
return projection
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { ItemProjection } from './ItemProjection'
|
||||
|
||||
export type ItemProjectionWithUser = ItemProjection & {
|
||||
user_uuid: string
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import 'reflect-metadata'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../Domain/Item/Item'
|
||||
import { ItemProjector } from './ItemProjector'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
describe('ItemProjector', () => {
|
||||
let item: Item
|
||||
let timer: TimerInterface
|
||||
|
||||
const createProjector = () => new ItemProjector(timer)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertMicrosecondsToStringDate = jest.fn().mockReturnValue('2021-04-15T08:00:00.123456Z')
|
||||
|
||||
item = new Item()
|
||||
item.uuid = '1-2-3'
|
||||
item.itemsKeyId = '2-3-4'
|
||||
item.duplicateOf = null
|
||||
item.encItemKey = '3-4-5'
|
||||
item.content = 'test'
|
||||
item.contentType = ContentType.Note
|
||||
item.authHash = 'asd'
|
||||
item.deleted = false
|
||||
item.createdAtTimestamp = 123
|
||||
item.updatedAtTimestamp = 123
|
||||
item.updatedWithSession = '7-6-5'
|
||||
item.userUuid = 'u1-2-3'
|
||||
})
|
||||
|
||||
it('should create a full projection of an item', async () => {
|
||||
expect(await createProjector().projectFull(item)).toMatchObject({
|
||||
uuid: '1-2-3',
|
||||
items_key_id: '2-3-4',
|
||||
duplicate_of: null,
|
||||
enc_item_key: '3-4-5',
|
||||
content: 'test',
|
||||
content_type: 'Note',
|
||||
auth_hash: 'asd',
|
||||
deleted: false,
|
||||
created_at: '2021-04-15T08:00:00.123456Z',
|
||||
updated_at: '2021-04-15T08:00:00.123456Z',
|
||||
updated_with_session: '7-6-5',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a custom projection of an item', async () => {
|
||||
expect(await createProjector().projectCustom('dump', item)).toMatchObject({
|
||||
uuid: '1-2-3',
|
||||
items_key_id: '2-3-4',
|
||||
duplicate_of: null,
|
||||
enc_item_key: '3-4-5',
|
||||
content: 'test',
|
||||
content_type: 'Note',
|
||||
auth_hash: 'asd',
|
||||
deleted: false,
|
||||
created_at: '2021-04-15T08:00:00.123456Z',
|
||||
updated_at: '2021-04-15T08:00:00.123456Z',
|
||||
updated_with_session: '7-6-5',
|
||||
user_uuid: 'u1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error on simple projection', async () => {
|
||||
let error = null
|
||||
try {
|
||||
await createProjector().projectSimple(item)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect((error as Error).message).toEqual('not implemented')
|
||||
})
|
||||
})
|
|
@ -1,41 +0,0 @@
|
|||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { ProjectorInterface } from './ProjectorInterface'
|
||||
|
||||
import { Item } from '../Domain/Item/Item'
|
||||
import { ItemProjection } from './ItemProjection'
|
||||
import { ItemProjectionWithUser } from './ItemProjectionWithUser'
|
||||
|
||||
export class ItemProjector implements ProjectorInterface<Item, ItemProjection> {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
async projectSimple(_item: Item): Promise<ItemProjection> {
|
||||
throw Error('not implemented')
|
||||
}
|
||||
|
||||
async projectCustom(_projectionType: string, item: Item): Promise<ItemProjectionWithUser> {
|
||||
const fullProjection = await this.projectFull(item)
|
||||
|
||||
return {
|
||||
...fullProjection,
|
||||
user_uuid: item.userUuid,
|
||||
}
|
||||
}
|
||||
|
||||
async projectFull(item: Item): Promise<ItemProjection> {
|
||||
return {
|
||||
uuid: item.uuid,
|
||||
items_key_id: item.itemsKeyId,
|
||||
duplicate_of: item.duplicateOf,
|
||||
enc_item_key: item.encItemKey,
|
||||
content: item.content,
|
||||
content_type: item.contentType as string,
|
||||
auth_hash: item.authHash,
|
||||
deleted: !!item.deleted,
|
||||
created_at: this.timer.convertMicrosecondsToStringDate(item.createdAtTimestamp),
|
||||
created_at_timestamp: item.createdAtTimestamp,
|
||||
updated_at: this.timer.convertMicrosecondsToStringDate(item.updatedAtTimestamp),
|
||||
updated_at_timestamp: item.updatedAtTimestamp,
|
||||
updated_with_session: item.updatedWithSession,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export interface ProjectorInterface<T, E> {
|
||||
projectSimple(object: T): Promise<Partial<E>>
|
||||
projectFull(object: T): Promise<E>
|
||||
projectCustom(projectionType: string, object: T, ...args: any[]): Promise<E>
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import 'reflect-metadata'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../Domain/Item/Item'
|
||||
import { SavedItemProjector } from './SavedItemProjector'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
describe('SavedItemProjector', () => {
|
||||
let item: Item
|
||||
let timer: TimerInterface
|
||||
|
||||
const createProjector = () => new SavedItemProjector(timer)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertMicrosecondsToStringDate = jest.fn().mockReturnValue('2021-04-15T08:00:00.123456Z')
|
||||
|
||||
item = new Item()
|
||||
item.uuid = '1-2-3'
|
||||
item.itemsKeyId = '2-3-4'
|
||||
item.duplicateOf = null
|
||||
item.encItemKey = '3-4-5'
|
||||
item.content = 'test'
|
||||
item.contentType = ContentType.Note
|
||||
item.authHash = 'asd'
|
||||
item.deleted = false
|
||||
item.createdAtTimestamp = 123
|
||||
item.updatedAtTimestamp = 123
|
||||
})
|
||||
|
||||
it('should create a full projection of an item', async () => {
|
||||
expect(await createProjector().projectFull(item)).toEqual({
|
||||
uuid: '1-2-3',
|
||||
duplicate_of: null,
|
||||
content_type: 'Note',
|
||||
auth_hash: 'asd',
|
||||
deleted: false,
|
||||
created_at: '2021-04-15T08:00:00.123456Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at: '2021-04-15T08:00:00.123456Z',
|
||||
updated_at_timestamp: 123,
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error on custom projection', async () => {
|
||||
let error = null
|
||||
try {
|
||||
await createProjector().projectCustom('test', item)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect((error as Error).message).toEqual('not implemented')
|
||||
})
|
||||
|
||||
it('should throw error on simple projection', async () => {
|
||||
let error = null
|
||||
try {
|
||||
await createProjector().projectSimple(item)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect((error as Error).message).toEqual('not implemented')
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue