mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
feat: add mechanism for determining if a user should use the primary or secondary items database (#700)
* feat(domain-core): introduce new role for users transitioning to new mechanisms * feat: add mechanism for determining if a user should use the primary or secondary items database * fix: add transition mode enabled switch in docker entrypoint * fix(syncing-server): mapping roles from middleware * fix: mongodb item repository binding * fix: item backups service binding * fix: passing transition mode enabled variable to docker setup
This commit is contained in:
@@ -24,6 +24,7 @@ services:
|
||||
DB_TYPE: "${DB_TYPE}"
|
||||
CACHE_TYPE: "${CACHE_TYPE}"
|
||||
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
|
||||
TRANSITION_MODE_ENABLED: "${TRANSITION_MODE_ENABLED}"
|
||||
container_name: server-ci
|
||||
ports:
|
||||
- 3123:3000
|
||||
|
||||
@@ -66,6 +66,9 @@ fi
|
||||
if [ -z "$SECONDARY_DB_ENABLED" ]; then
|
||||
export SECONDARY_DB_ENABLED=false
|
||||
fi
|
||||
if [ -z "$TRANSITION_MODE_ENABLED" ]; then
|
||||
export TRANSITION_MODE_ENABLED=false
|
||||
fi
|
||||
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
|
||||
|
||||
#########
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348191367 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348280258 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -560,6 +560,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind(TYPES.Auth_READONLY_USERS)
|
||||
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
|
||||
container
|
||||
.bind(TYPES.Auth_TRANSITION_MODE_ENABLED)
|
||||
.toConstantValue(env.get('TRANSITION_MODE_ENABLED', true) === 'true')
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
|
||||
@@ -101,6 +101,7 @@ const TYPES = {
|
||||
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
|
||||
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
|
||||
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
|
||||
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
|
||||
// use cases
|
||||
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
|
||||
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Register } from './Register'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
|
||||
import { Session } from '../Session/Session'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
describe('Register', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -20,9 +21,19 @@ describe('Register', () => {
|
||||
let user: User
|
||||
let crypter: CrypterInterface
|
||||
let timer: TimerInterface
|
||||
let transitionModeEnabled = false
|
||||
|
||||
const createUseCase = () =>
|
||||
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer)
|
||||
new Register(
|
||||
userRepository,
|
||||
roleRepository,
|
||||
authResponseFactory,
|
||||
crypter,
|
||||
false,
|
||||
settingService,
|
||||
timer,
|
||||
transitionModeEnabled,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
@@ -75,6 +86,7 @@ describe('Register', () => {
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
uuid: expect.any(String),
|
||||
version: '004',
|
||||
roles: Promise.resolve([]),
|
||||
createdAt: new Date(1),
|
||||
updatedAt: new Date(1),
|
||||
})
|
||||
@@ -118,6 +130,48 @@ describe('Register', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should register a new user with default role and transition role', async () => {
|
||||
transitionModeEnabled = true
|
||||
|
||||
const role = new Role()
|
||||
role.name = RoleName.NAMES.CoreUser
|
||||
|
||||
const transitionRole = new Role()
|
||||
transitionRole.name = RoleName.NAMES.TransitionUser
|
||||
|
||||
roleRepository.findOneByName = jest.fn().mockReturnValueOnce(role).mockReturnValueOnce(transitionRole)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
apiVersion: '20200115',
|
||||
ephemeralSession: false,
|
||||
version: '004',
|
||||
pwCost: 11,
|
||||
pwSalt: 'qweqwe',
|
||||
pwNonce: undefined,
|
||||
}),
|
||||
).toEqual({ success: true, authResponse: { foo: 'bar' } })
|
||||
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
email: 'test@test.te',
|
||||
encryptedPassword: expect.any(String),
|
||||
encryptedServerKey: 'test',
|
||||
serverEncryptionVersion: 1,
|
||||
pwCost: 11,
|
||||
pwNonce: undefined,
|
||||
pwSalt: 'qweqwe',
|
||||
updatedWithUserAgent: 'Mozilla',
|
||||
uuid: expect.any(String),
|
||||
version: '004',
|
||||
createdAt: new Date(1),
|
||||
updatedAt: new Date(1),
|
||||
roles: Promise.resolve([role, transitionRole]),
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail to register if username is invalid', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
@@ -195,6 +249,7 @@ describe('Register', () => {
|
||||
true,
|
||||
settingService,
|
||||
timer,
|
||||
transitionModeEnabled,
|
||||
).execute({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as bcrypt from 'bcryptjs'
|
||||
import { RoleName, Username } from '@standardnotes/domain-core'
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
@@ -11,7 +12,6 @@ import { RegisterResponse } from './RegisterResponse'
|
||||
import { UseCaseInterface } from './UseCaseInterface'
|
||||
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
|
||||
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
|
||||
@@ -27,6 +27,7 @@ export class Register implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
|
||||
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_TRANSITION_MODE_ENABLED) private transitionModeEnabled: boolean,
|
||||
) {}
|
||||
|
||||
async execute(dto: RegisterDTO): Promise<RegisterResponse> {
|
||||
@@ -72,10 +73,18 @@ export class Register implements UseCaseInterface {
|
||||
user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey()
|
||||
user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION
|
||||
|
||||
const roles = []
|
||||
const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser)
|
||||
if (defaultRole) {
|
||||
user.roles = Promise.resolve([defaultRole])
|
||||
roles.push(defaultRole)
|
||||
}
|
||||
if (this.transitionModeEnabled) {
|
||||
const transitionRole = await this.roleRepository.findOneByName(RoleName.NAMES.TransitionUser)
|
||||
if (transitionRole) {
|
||||
roles.push(transitionRole)
|
||||
}
|
||||
}
|
||||
user.roles = Promise.resolve(roles)
|
||||
|
||||
Object.assign(user, registrationFields)
|
||||
|
||||
|
||||
@@ -21,25 +21,36 @@ describe('RoleName', () => {
|
||||
const plusUserRole = RoleName.create(RoleName.NAMES.PlusUser).getValue()
|
||||
const coreUser = RoleName.create(RoleName.NAMES.CoreUser).getValue()
|
||||
const internalTeamUser = RoleName.create(RoleName.NAMES.InternalTeamUser).getValue()
|
||||
const transitionUser = RoleName.create(RoleName.NAMES.TransitionUser).getValue()
|
||||
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(internalTeamUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(proUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(plusUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(coreUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
|
||||
expect(transitionUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export class RoleName extends ValueObject<RoleNameProps> {
|
||||
PlusUser: 'PLUS_USER',
|
||||
ProUser: 'PRO_USER',
|
||||
InternalTeamUser: 'INTERNAL_TEAM_USER',
|
||||
TransitionUser: 'TRANSITION_USER',
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
@@ -19,11 +20,19 @@ export class RoleName extends ValueObject<RoleNameProps> {
|
||||
case RoleName.NAMES.InternalTeamUser:
|
||||
return true
|
||||
case RoleName.NAMES.ProUser:
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser].includes(roleName.value)
|
||||
return [
|
||||
RoleName.NAMES.CoreUser,
|
||||
RoleName.NAMES.PlusUser,
|
||||
RoleName.NAMES.ProUser,
|
||||
RoleName.NAMES.TransitionUser,
|
||||
].includes(roleName.value)
|
||||
case RoleName.NAMES.PlusUser:
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser].includes(roleName.value)
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.TransitionUser].includes(
|
||||
roleName.value,
|
||||
)
|
||||
case RoleName.NAMES.CoreUser:
|
||||
return [RoleName.NAMES.CoreUser].includes(roleName.value)
|
||||
case RoleName.NAMES.TransitionUser:
|
||||
return [RoleName.NAMES.CoreUser, RoleName.NAMES.TransitionUser].includes(roleName.value)
|
||||
/*istanbul ignore next*/
|
||||
default:
|
||||
throw new Error(`Invalid role name: ${this.value}`)
|
||||
|
||||
@@ -3,32 +3,24 @@ import { RoleNameCollection } from './RoleNameCollection'
|
||||
|
||||
describe('RoleNameCollection', () => {
|
||||
it('should create a value object', () => {
|
||||
const role1 = RoleName.create(RoleName.NAMES.ProUser).getValue()
|
||||
|
||||
const valueOrError = RoleNameCollection.create([role1])
|
||||
const valueOrError = RoleNameCollection.create([RoleName.NAMES.ProUser])
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().value).toEqual([role1])
|
||||
expect(valueOrError.getValue().value[0].value).toEqual('PRO_USER')
|
||||
})
|
||||
|
||||
it('should tell if collections are not equal', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
let roles2 = RoleNameCollection.create([
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
|
||||
]).getValue()
|
||||
let roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser]).getValue()
|
||||
|
||||
let valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
|
||||
|
||||
roles2 = RoleNameCollection.create([
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
|
||||
RoleName.NAMES.ProUser,
|
||||
RoleName.NAMES.PlusUser,
|
||||
RoleName.NAMES.CoreUser,
|
||||
]).getValue()
|
||||
|
||||
valueOrError = RoleNameCollection.create(roles1)
|
||||
@@ -36,42 +28,30 @@ describe('RoleNameCollection', () => {
|
||||
})
|
||||
|
||||
it('should tell if collections are equal', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
const roles2 = RoleNameCollection.create([
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]).getValue()
|
||||
const roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]).getValue()
|
||||
|
||||
const valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().equals(roles2)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell if collection includes element', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
const valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.ProUser).getValue())).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell if collection does not includes element', () => {
|
||||
const roles1 = [
|
||||
RoleName.create(RoleName.NAMES.ProUser).getValue(),
|
||||
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
|
||||
]
|
||||
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
|
||||
const valueOrError = RoleNameCollection.create(roles1)
|
||||
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.CoreUser).getValue())).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell if collection has a role with more or equal power to', () => {
|
||||
let roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue()]
|
||||
let roles = [RoleName.NAMES.CoreUser]
|
||||
let valueOrError = RoleNameCollection.create(roles)
|
||||
let roleNames = valueOrError.getValue()
|
||||
|
||||
@@ -83,7 +63,7 @@ describe('RoleNameCollection', () => {
|
||||
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
|
||||
).toBeTruthy()
|
||||
|
||||
roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
|
||||
roles = [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]
|
||||
valueOrError = RoleNameCollection.create(roles)
|
||||
roleNames = valueOrError.getValue()
|
||||
|
||||
@@ -95,7 +75,7 @@ describe('RoleNameCollection', () => {
|
||||
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
|
||||
).toBeTruthy()
|
||||
|
||||
roles = [RoleName.create(RoleName.NAMES.ProUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
|
||||
roles = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
|
||||
valueOrError = RoleNameCollection.create(roles)
|
||||
roleNames = valueOrError.getValue()
|
||||
|
||||
@@ -109,4 +89,11 @@ describe('RoleNameCollection', () => {
|
||||
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail to create a collection if a role name is invalid', () => {
|
||||
const valueOrError = RoleNameCollection.create(['invalid-role-name'])
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
expect(valueOrError.getError()).toEqual('Invalid role name: invalid-role-name')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +46,16 @@ export class RoleNameCollection extends ValueObject<RoleNameCollectionProps> {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(roleName: RoleName[]): Result<RoleNameCollection> {
|
||||
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleName }))
|
||||
static create(roleNameStrings: string[]): Result<RoleNameCollection> {
|
||||
const roleNames: RoleName[] = []
|
||||
for (const roleNameString of roleNameStrings) {
|
||||
const roleNameOrError = RoleName.create(roleNameString)
|
||||
if (roleNameOrError.isFailed()) {
|
||||
return Result.fail<RoleNameCollection>(roleNameOrError.getError())
|
||||
}
|
||||
roleNames.push(roleNameOrError.getValue())
|
||||
}
|
||||
|
||||
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleNames }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,5 @@ MONGO_PORT=27017
|
||||
MONGO_USERNAME=standardnotes
|
||||
MONGO_PASSWORD=standardnotes
|
||||
MONGO_DATABASE=standardnotes
|
||||
|
||||
TRANSITION_MODE_ENABLED=false
|
||||
|
||||
@@ -39,7 +39,7 @@ import { SyncItems } from '../Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { InversifyExpressAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware'
|
||||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
|
||||
import { ContentDecoder } from '@standardnotes/common'
|
||||
import { ContentDecoder, ContentDecoderInterface } from '@standardnotes/common'
|
||||
import {
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventHandlerInterface,
|
||||
@@ -153,6 +153,9 @@ import { AddNotificationsForUsers } from '../Domain/UseCase/Messaging/AddNotific
|
||||
import { MongoDBItem } from '../Infra/TypeORM/MongoDBItem'
|
||||
import { MongoDBItemRepository } from '../Infra/TypeORM/MongoDBItemRepository'
|
||||
import { MongoDBItemPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBItemPersistenceMapper'
|
||||
import { Logger } from 'winston'
|
||||
import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
|
||||
import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -284,6 +287,18 @@ export class ContainerConfigLoader {
|
||||
})
|
||||
}
|
||||
|
||||
container
|
||||
.bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
|
||||
.toConstantValue(
|
||||
env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) ? +env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) : 10485760,
|
||||
)
|
||||
container.bind(TYPES.Sync_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||
container
|
||||
.bind(TYPES.Sync_FILE_UPLOAD_PATH)
|
||||
.toConstantValue(
|
||||
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
|
||||
)
|
||||
|
||||
// Mapping
|
||||
container
|
||||
.bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
|
||||
@@ -374,25 +389,37 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(new MongoDBItemPersistenceMapper())
|
||||
|
||||
container
|
||||
.bind<MongoRepository<MongoDBItem>>(TYPES.Sync_MongoItemRepository)
|
||||
.bind<MongoRepository<MongoDBItem>>(TYPES.Sync_ORMMongoItemRepository)
|
||||
.toConstantValue(appDataSource.getMongoRepository(MongoDBItem))
|
||||
|
||||
container
|
||||
.bind<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
|
||||
.toConstantValue(
|
||||
new MongoDBItemRepository(
|
||||
container.get<MongoRepository<MongoDBItem>>(TYPES.Sync_ORMMongoItemRepository),
|
||||
container.get<MapperInterface<Item, MongoDBItem>>(TYPES.Sync_MongoDBItemPersistenceMapper),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Repositories
|
||||
container
|
||||
.bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository)
|
||||
.bind<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository)
|
||||
.toConstantValue(
|
||||
isSecondaryDatabaseEnabled
|
||||
? new MongoDBItemRepository(
|
||||
container.get(TYPES.Sync_MongoItemRepository),
|
||||
container.get(TYPES.Sync_MongoDBItemPersistenceMapper),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
: new TypeORMItemRepository(
|
||||
container.get(TYPES.Sync_ORMItemRepository),
|
||||
container.get(TYPES.Sync_ItemPersistenceMapper),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
),
|
||||
new TypeORMItemRepository(
|
||||
container.get<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository),
|
||||
container.get<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver)
|
||||
.toConstantValue(
|
||||
new TypeORMItemRepositoryResolver(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository)
|
||||
@@ -444,10 +471,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<ItemTransferCalculatorInterface>(TYPES.Sync_ItemTransferCalculator)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemTransferCalculator(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
return new ItemTransferCalculator(context.container.get<Logger>(TYPES.Sync_Logger))
|
||||
})
|
||||
|
||||
// Middleware
|
||||
@@ -525,7 +549,7 @@ export class ContainerConfigLoader {
|
||||
.bind<GetItems>(TYPES.Sync_GetItems)
|
||||
.toConstantValue(
|
||||
new GetItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
@@ -537,7 +561,7 @@ export class ContainerConfigLoader {
|
||||
.bind<SaveNewItem>(TYPES.Sync_SaveNewItem)
|
||||
.toConstantValue(
|
||||
new SaveNewItem(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_DomainEventPublisher),
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
@@ -563,7 +587,7 @@ export class ContainerConfigLoader {
|
||||
.bind<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem)
|
||||
.toConstantValue(
|
||||
new UpdateExistingItem(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_DomainEventPublisher),
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
@@ -578,7 +602,7 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new SaveItems(
|
||||
container.get(TYPES.Sync_ItemSaveValidator),
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_SaveNewItem),
|
||||
container.get(TYPES.Sync_UpdateExistingItem),
|
||||
@@ -607,7 +631,7 @@ export class ContainerConfigLoader {
|
||||
.bind<SyncItems>(TYPES.Sync_SyncItems)
|
||||
.toConstantValue(
|
||||
new SyncItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get(TYPES.Sync_GetItems),
|
||||
container.get(TYPES.Sync_SaveItems),
|
||||
container.get(TYPES.Sync_GetSharedVaults),
|
||||
@@ -617,10 +641,10 @@ export class ContainerConfigLoader {
|
||||
),
|
||||
)
|
||||
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepository))
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepositoryResolver))
|
||||
})
|
||||
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepositoryResolver))
|
||||
})
|
||||
container
|
||||
.bind<InviteUserToSharedVault>(TYPES.Sync_InviteUserToSharedVault)
|
||||
@@ -778,48 +802,56 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
})
|
||||
|
||||
// env vars
|
||||
container
|
||||
.bind(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE)
|
||||
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
|
||||
.toConstantValue(
|
||||
env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) ? +env.get('EMAIL_ATTACHMENT_MAX_BYTE_SIZE', true) : 10485760,
|
||||
)
|
||||
container.bind(TYPES.Sync_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||
container
|
||||
.bind(TYPES.Sync_FILE_UPLOAD_PATH)
|
||||
.toConstantValue(
|
||||
env.get('FILE_UPLOAD_PATH', true) ? env.get('FILE_UPLOAD_PATH', true) : this.DEFAULT_FILE_UPLOAD_PATH,
|
||||
env.get('S3_AWS_REGION', true)
|
||||
? new S3ItemBackupService(
|
||||
container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
container.get(TYPES.Sync_ItemBackupMapper),
|
||||
container.get(TYPES.Sync_ItemHttpMapper),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
container.get(TYPES.Sync_S3),
|
||||
)
|
||||
: new FSItemBackupService(
|
||||
container.get(TYPES.Sync_FILE_UPLOAD_PATH),
|
||||
container.get(TYPES.Sync_ItemBackupMapper),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
// Handlers
|
||||
container
|
||||
.bind<DuplicateItemSyncedEventHandler>(TYPES.Sync_DuplicateItemSyncedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new DuplicateItemSyncedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new DuplicateItemSyncedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.Sync_AccountDeletionRequestedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new AccountDeletionRequestedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemRevisionCreationRequestedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_ItemBackupService),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new ItemRevisionCreationRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileUploadedEventHandler>(TYPES.Sync_SharedVaultFileUploadedEventHandler)
|
||||
.toConstantValue(
|
||||
@@ -844,38 +876,17 @@ export class ContainerConfigLoader {
|
||||
container.bind<AxiosInstance>(TYPES.Sync_HTTPClient).toDynamicValue(() => axios.create())
|
||||
container
|
||||
.bind<ExtensionsHttpServiceInterface>(TYPES.Sync_ExtensionsHttpService)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ExtensionsHttpService(
|
||||
context.container.get(TYPES.Sync_HTTPClient),
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_ContentDecoder),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
|
||||
container
|
||||
.bind<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
const env: Env = context.container.get(TYPES.Sync_Env)
|
||||
|
||||
if (env.get('S3_AWS_REGION', true)) {
|
||||
return new S3ItemBackupService(
|
||||
context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
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_ItemBackupMapper),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
}
|
||||
})
|
||||
.toConstantValue(
|
||||
new ExtensionsHttpService(
|
||||
container.get<AxiosInstance>(TYPES.Sync_HTTPClient),
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
|
||||
container.get<ContentDecoderInterface>(TYPES.Sync_ContentDecoder),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['DUPLICATE_ITEM_SYNCED', container.get(TYPES.Sync_DuplicateItemSyncedEventHandler)],
|
||||
@@ -904,19 +915,22 @@ export class ContainerConfigLoader {
|
||||
|
||||
container
|
||||
.bind<EmailBackupRequestedEventHandler>(TYPES.Sync_EmailBackupRequestedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new EmailBackupRequestedEventHandler(
|
||||
context.container.get(TYPES.Sync_ItemRepository),
|
||||
context.container.get(TYPES.Sync_AuthHttpService),
|
||||
context.container.get(TYPES.Sync_ItemBackupService),
|
||||
context.container.get(TYPES.Sync_DomainEventPublisher),
|
||||
context.container.get(TYPES.Sync_DomainEventFactory),
|
||||
context.container.get(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE),
|
||||
context.container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
context.container.get(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
context.container.get(TYPES.Sync_Logger),
|
||||
)
|
||||
})
|
||||
.toConstantValue(
|
||||
new EmailBackupRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
|
||||
isSecondaryDatabaseEnabled
|
||||
? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
|
||||
: null,
|
||||
container.get<AuthHttpServiceInterface>(TYPES.Sync_AuthHttpService),
|
||||
container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<number>(TYPES.Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE),
|
||||
container.get<ItemTransferCalculatorInterface>(TYPES.Sync_ItemTransferCalculator),
|
||||
container.get<string>(TYPES.Sync_S3_BACKUP_BUCKET_NAME),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
eventHandlers.set('EMAIL_BACKUP_REQUESTED', container.get(TYPES.Sync_EmailBackupRequestedEventHandler))
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ const TYPES = {
|
||||
Sync_S3: Symbol.for('Sync_S3'),
|
||||
Sync_Env: Symbol.for('Sync_Env'),
|
||||
// Repositories
|
||||
Sync_ItemRepository: Symbol.for('Sync_ItemRepository'),
|
||||
Sync_ItemRepositoryResolver: Symbol.for('Sync_ItemRepositoryResolver'),
|
||||
Sync_MySQLItemRepository: Symbol.for('Sync_MySQLItemRepository'),
|
||||
Sync_MongoDBItemRepository: Symbol.for('Sync_MongoDBItemRepository'),
|
||||
Sync_SharedVaultRepository: Symbol.for('Sync_SharedVaultRepository'),
|
||||
Sync_SharedVaultInviteRepository: Symbol.for('Sync_SharedVaultInviteRepository'),
|
||||
Sync_SharedVaultUserRepository: Symbol.for('Sync_SharedVaultUserRepository'),
|
||||
@@ -23,7 +25,7 @@ const TYPES = {
|
||||
Sync_ORMNotificationRepository: Symbol.for('Sync_ORMNotificationRepository'),
|
||||
Sync_ORMMessageRepository: Symbol.for('Sync_ORMMessageRepository'),
|
||||
// Mongo
|
||||
Sync_MongoItemRepository: Symbol.for('Sync_MongoItemRepository'),
|
||||
Sync_ORMMongoItemRepository: Symbol.for('Sync_ORMMongoItemRepository'),
|
||||
// Middleware
|
||||
Sync_AuthMiddleware: Symbol.for('Sync_AuthMiddleware'),
|
||||
// env vars
|
||||
|
||||
@@ -13,7 +13,8 @@ import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardn
|
||||
|
||||
describe('ExtensionsHttpService', () => {
|
||||
let httpClient: AxiosInstance
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let contentDecoder: ContentDecoderInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
@@ -24,7 +25,8 @@ describe('ExtensionsHttpService', () => {
|
||||
const createService = () =>
|
||||
new ExtensionsHttpService(
|
||||
httpClient,
|
||||
itemRepository,
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
contentDecoder,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
@@ -54,8 +56,8 @@ describe('ExtensionsHttpService', () => {
|
||||
|
||||
authParams = {} as jest.Mocked<KeyParamsData>
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
@@ -191,6 +193,31 @@ describe('ExtensionsHttpService', () => {
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should publish a failed backup event if the extension is in the secondary repository', async () => {
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
httpClient.request = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Could not reach the extensions server')
|
||||
})
|
||||
|
||||
await createService().sendItemsToExtensionsServer({
|
||||
userUuid: '1-2-3',
|
||||
extensionId: '2-3-4',
|
||||
extensionsServerUrl: '',
|
||||
forceMute: false,
|
||||
items: [item],
|
||||
backupFilename: 'backup-file',
|
||||
authParams,
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should publish a failed Dropbox backup event if request was sent and extensions server responded not ok', async () => {
|
||||
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
|
||||
|
||||
@@ -273,7 +300,7 @@ describe('ExtensionsHttpService', () => {
|
||||
})
|
||||
|
||||
it('should throw an error if the extension to post to is not found', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
httpClient.request = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Could not reach the extensions server')
|
||||
@@ -299,7 +326,7 @@ describe('ExtensionsHttpService', () => {
|
||||
|
||||
it('should throw an error if the extension to post to has no content', async () => {
|
||||
item = {} as jest.Mocked<Item>
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
httpClient.request = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Could not reach the extensions server')
|
||||
|
||||
@@ -17,7 +17,8 @@ import { getBody as oneDriveBody, getSubject as oneDriveSubject } from '../Email
|
||||
export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
|
||||
constructor(
|
||||
private httpClient: AxiosInstance,
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private contentDecoder: ContentDecoderInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
@@ -139,9 +140,14 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
|
||||
userUuid: string,
|
||||
email: string,
|
||||
): Promise<DomainEventInterface> {
|
||||
const extension = await this.itemRepository.findByUuidAndUserUuid(extensionId, userUuid)
|
||||
let extension = await this.primaryItemRepository.findByUuidAndUserUuid(extensionId, userUuid)
|
||||
if (extension === null || !extension.props.content) {
|
||||
throw Error(`Could not find extensions with id ${extensionId}`)
|
||||
if (this.secondaryItemRepository) {
|
||||
extension = await this.secondaryItemRepository.findByUuidAndUserUuid(extensionId, userUuid)
|
||||
}
|
||||
if (extension === null || !extension.props.content) {
|
||||
throw Error(`Could not find extensions with id ${extensionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const content = this.contentDecoder.decode(extension.props.content)
|
||||
|
||||
@@ -8,12 +8,14 @@ import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequested
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let logger: Logger
|
||||
let event: AccountDeletionRequestedEvent
|
||||
let item: Item
|
||||
|
||||
const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepository, logger)
|
||||
const createHandler = () =>
|
||||
new AccountDeletionRequestedEventHandler(primaryItemRepository, secondaryItemRepository, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
item = Item.create(
|
||||
@@ -33,9 +35,9 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
itemRepository.deleteByUserUuid = jest.fn()
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
primaryItemRepository.deleteByUserUuid = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
@@ -52,6 +54,17 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
it('should remove all items for a user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(itemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
|
||||
expect(primaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
|
||||
})
|
||||
|
||||
it('should remove all items for a user from secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.deleteByUserUuid = jest.fn()
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(secondaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,10 +3,17 @@ import { Logger } from 'winston'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
|
||||
constructor(
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
await this.itemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
await this.primaryItemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.secondaryItemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
}
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
describe('DuplicateItemSyncedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let logger: Logger
|
||||
let duplicateItem: Item
|
||||
let originalItem: Item
|
||||
@@ -22,7 +23,13 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new DuplicateItemSyncedEventHandler(itemRepository, domainEventFactory, domainEventPublisher, logger)
|
||||
new DuplicateItemSyncedEventHandler(
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
originalItem = Item.create(
|
||||
@@ -59,8 +66,8 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuidAndUserUuid = jest
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(duplicateItem)
|
||||
.mockReturnValueOnce(originalItem)
|
||||
@@ -90,8 +97,22 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should copy revisions from original item to the duplicate item in the secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findByUuidAndUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(duplicateItem)
|
||||
.mockReturnValueOnce(originalItem)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should not copy revisions if original item does not exist', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
@@ -99,7 +120,7 @@ describe('DuplicateItemSyncedEventHandler', () => {
|
||||
})
|
||||
|
||||
it('should not copy revisions if duplicate item does not exist', async () => {
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
|
||||
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
|
||||
@@ -9,14 +9,26 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
|
||||
export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: DuplicateItemSyncedEvent): Promise<void> {
|
||||
const item = await this.itemRepository.findByUuidAndUserUuid(event.payload.itemUuid, event.payload.userUuid)
|
||||
await this.requestRevisionsCopy(event, this.primaryItemRepository)
|
||||
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.requestRevisionsCopy(event, this.secondaryItemRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private async requestRevisionsCopy(
|
||||
event: DuplicateItemSyncedEvent,
|
||||
itemRepository: ItemRepositoryInterface,
|
||||
): Promise<void> {
|
||||
const item = await itemRepository.findByUuidAndUserUuid(event.payload.itemUuid, event.payload.userUuid)
|
||||
|
||||
if (item === null) {
|
||||
this.logger.warn(`Could not find item with uuid ${event.payload.itemUuid}`)
|
||||
@@ -30,7 +42,7 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
|
||||
return
|
||||
}
|
||||
|
||||
const existingOriginalItem = await this.itemRepository.findByUuidAndUserUuid(
|
||||
const existingOriginalItem = await itemRepository.findByUuidAndUserUuid(
|
||||
item.props.duplicateOf.value,
|
||||
event.payload.userUuid,
|
||||
)
|
||||
|
||||
@@ -13,9 +13,11 @@ import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { EmailBackupRequestedEventHandler } from './EmailBackupRequestedEventHandler'
|
||||
import { ItemTransferCalculatorInterface } from '../Item/ItemTransferCalculatorInterface'
|
||||
import { ItemContentSizeDescriptor } from '../Item/ItemContentSizeDescriptor'
|
||||
|
||||
describe('EmailBackupRequestedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let authHttpService: AuthHttpServiceInterface
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
@@ -28,7 +30,8 @@ describe('EmailBackupRequestedEventHandler', () => {
|
||||
|
||||
const createHandler = () =>
|
||||
new EmailBackupRequestedEventHandler(
|
||||
itemRepository,
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
authHttpService,
|
||||
itemBackupService,
|
||||
domainEventPublisher,
|
||||
@@ -42,8 +45,11 @@ describe('EmailBackupRequestedEventHandler', () => {
|
||||
beforeEach(() => {
|
||||
item = {} as jest.Mocked<Item>
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
primaryItemRepository.findContentSizeForComputingTransferLimit = jest
|
||||
.fn()
|
||||
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
|
||||
|
||||
authHttpService = {} as jest.Mocked<AuthHttpServiceInterface>
|
||||
authHttpService.getUserKeyParams = jest.fn().mockReturnValue({ identifier: 'test@test.com' })
|
||||
@@ -81,6 +87,21 @@ describe('EmailBackupRequestedEventHandler', () => {
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should inform that backup attachment for email was created in the secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
secondaryItemRepository.findContentSizeForComputingTransferLimit = jest
|
||||
.fn()
|
||||
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalledTimes(2)
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should inform that multipart backup attachment for email was created', async () => {
|
||||
itemBackupService.backup = jest
|
||||
.fn()
|
||||
|
||||
@@ -16,7 +16,8 @@ import { getBody, getSubject } from '../Email/EmailBackupAttachmentCreated'
|
||||
|
||||
export class EmailBackupRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private authHttpService: AuthHttpServiceInterface,
|
||||
private itemBackupService: ItemBackupServiceInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@@ -28,6 +29,17 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
) {}
|
||||
|
||||
async handle(event: EmailBackupRequestedEvent): Promise<void> {
|
||||
await this.requestEmailWithBackupFile(event, this.primaryItemRepository)
|
||||
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.requestEmailWithBackupFile(event, this.secondaryItemRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private async requestEmailWithBackupFile(
|
||||
event: EmailBackupRequestedEvent,
|
||||
itemRepository: ItemRepositoryInterface,
|
||||
): Promise<void> {
|
||||
let authParams: KeyParamsData
|
||||
try {
|
||||
authParams = await this.authHttpService.getUserKeyParams({
|
||||
@@ -46,14 +58,15 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
sortOrder: 'ASC',
|
||||
deleted: false,
|
||||
}
|
||||
const itemContentSizeDescriptors = await itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
|
||||
const itemUuidBundles = await this.itemTransferCalculator.computeItemUuidBundlesToFetch(
|
||||
itemQuery,
|
||||
itemContentSizeDescriptors,
|
||||
this.emailAttachmentMaxByteSize,
|
||||
)
|
||||
|
||||
const backupFileNames: string[] = []
|
||||
for (const itemUuidBundle of itemUuidBundles) {
|
||||
const items = await this.itemRepository.findAll({
|
||||
const items = await itemRepository.findAll({
|
||||
uuids: itemUuidBundle,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
|
||||
@@ -14,7 +14,8 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let primaryItemRepository: ItemRepositoryInterface
|
||||
let secondaryItemRepository: ItemRepositoryInterface | null
|
||||
let event: ItemRevisionCreationRequestedEvent
|
||||
let item: Item
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
@@ -23,7 +24,8 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
|
||||
const createHandler = () =>
|
||||
new ItemRevisionCreationRequestedEventHandler(
|
||||
itemRepository,
|
||||
primaryItemRepository,
|
||||
secondaryItemRepository,
|
||||
itemBackupService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
@@ -47,8 +49,8 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
primaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
event = {} as jest.Mocked<ItemRevisionCreationRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
@@ -80,8 +82,20 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create a revision for an item in the secondary repository', async () => {
|
||||
secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
secondaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
|
||||
|
||||
secondaryItemRepository = null
|
||||
})
|
||||
|
||||
it('should not create a revision for an item that does not exist', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
primaryItemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
|
||||
@@ -11,20 +11,32 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
|
||||
export class ItemRevisionCreationRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private primaryItemRepository: ItemRepositoryInterface,
|
||||
private secondaryItemRepository: ItemRepositoryInterface | null,
|
||||
private itemBackupService: ItemBackupServiceInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
|
||||
await this.createItemDump(event, this.primaryItemRepository)
|
||||
|
||||
if (this.secondaryItemRepository) {
|
||||
await this.createItemDump(event, this.secondaryItemRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private async createItemDump(
|
||||
event: ItemRevisionCreationRequestedEvent,
|
||||
itemRepository: ItemRepositoryInterface,
|
||||
): Promise<void> {
|
||||
const itemUuidOrError = Uuid.create(event.payload.itemUuid)
|
||||
if (itemUuidOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const itemUuid = itemUuidOrError.getValue()
|
||||
|
||||
const item = await this.itemRepository.findByUuid(itemUuid)
|
||||
const item = await itemRepository.findByUuid(itemUuid)
|
||||
if (item === null) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
describe('ItemContentSizeDescriptor', () => {
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return error if shared vault uuid is not valid', () => {
|
||||
const valueOrError = ItemContentSizeDescriptor.create('invalid', 20)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Result, Uuid, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemContentSizeDescriptorProps } from './ItemContentSizeDescriptorProps'
|
||||
|
||||
export class ItemContentSizeDescriptor extends ValueObject<ItemContentSizeDescriptorProps> {
|
||||
private constructor(props: ItemContentSizeDescriptorProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(itemUuidString: string, contentSize: number | null): Result<ItemContentSizeDescriptor> {
|
||||
const uuidOrError = Uuid.create(itemUuidString)
|
||||
if (uuidOrError.isFailed()) {
|
||||
return Result.fail<ItemContentSizeDescriptor>(uuidOrError.getError())
|
||||
}
|
||||
const uuid = uuidOrError.getValue()
|
||||
|
||||
return Result.ok<ItemContentSizeDescriptor>(
|
||||
new ItemContentSizeDescriptor({
|
||||
uuid,
|
||||
contentSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface ItemContentSizeDescriptorProps {
|
||||
uuid: Uuid
|
||||
contentSize: number | null
|
||||
}
|
||||
@@ -3,14 +3,13 @@ import { Uuid } from '@standardnotes/domain-core'
|
||||
import { Item } from './Item'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ExtendedIntegrityPayload } from './ExtendedIntegrityPayload'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export interface ItemRepositoryInterface {
|
||||
deleteByUserUuid(userUuid: string): Promise<void>
|
||||
findAll(query: ItemQuery): Promise<Item[]>
|
||||
countAll(query: ItemQuery): Promise<number>
|
||||
findContentSizeForComputingTransferLimit(
|
||||
query: ItemQuery,
|
||||
): Promise<Array<{ uuid: string; contentSize: number | null }>>
|
||||
findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<Array<ItemContentSizeDescriptor>>
|
||||
findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>>
|
||||
findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]>
|
||||
findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RoleNameCollection } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
|
||||
export interface ItemRepositoryResolverInterface {
|
||||
resolve(roleNames: RoleNameCollection): ItemRepositoryInterface
|
||||
}
|
||||
@@ -1,201 +1,143 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
|
||||
import { ItemTransferCalculator } from './ItemTransferCalculator'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
describe('ItemTransferCalculator', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createCalculator = () => new ItemTransferCalculator(itemRepository, logger)
|
||||
const createCalculator = () => new ItemTransferCalculator(logger)
|
||||
|
||||
beforeEach(() => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([])
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
describe('fetching uuids', () => {
|
||||
it('should compute uuids to fetch based on transfer limit - one item overlaping limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4', '3-4-5'])
|
||||
})
|
||||
|
||||
it('should compute uuids to fetch based on transfer limit - exact limit fit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
])
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 40)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4'])
|
||||
expect(result).toEqual(['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'])
|
||||
})
|
||||
|
||||
it('should compute uuids to fetch based on transfer limit - content size not defined on an item', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4', '3-4-5'])
|
||||
})
|
||||
|
||||
it('should compute uuids to fetch based on transfer limit - first item over the limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 50,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
])
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 50).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(query, 40)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual(['1-2-3', '2-3-4'])
|
||||
expect(result).toEqual(['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetching bundles', () => {
|
||||
it('should compute uuid bundles to fetch based on transfer limit - one item overlaping limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4', '3-4-5']])
|
||||
})
|
||||
|
||||
it('should compute uuid bundles to fetch based on transfer limit - exact limit fit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual([
|
||||
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
['00000000-0000-0000-0000-000000000002'],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 40)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4'], ['3-4-5']])
|
||||
})
|
||||
|
||||
it('should compute uuid bundles to fetch based on transfer limit - content size not defined on an item', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 50)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4', '3-4-5']])
|
||||
})
|
||||
|
||||
it('should compute uuid bundles to fetch based on transfer limit - first item over the limit', async () => {
|
||||
const query = {} as jest.Mocked<ItemQuery>
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest.fn().mockReturnValue([
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
contentSize: 50,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
contentSize: 20,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
contentSize: 20,
|
||||
},
|
||||
const itemContentSizeDescriptors = [
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 50).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000001', 20).getValue(),
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
|
||||
|
||||
expect(result).toEqual([
|
||||
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
['00000000-0000-0000-0000-000000000002'],
|
||||
])
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(query, 40)
|
||||
|
||||
expect(result).toEqual([['1-2-3', '2-3-4'], ['3-4-5']])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
constructor(private itemRepository: ItemRepositoryInterface, private logger: Logger) {}
|
||||
constructor(private logger: Logger) {}
|
||||
|
||||
async computeItemUuidsToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<string>> {
|
||||
async computeItemUuidsToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<string>> {
|
||||
const itemUuidsToFetch = []
|
||||
const itemContentSizes = await this.itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
|
||||
let totalContentSizeInBytes = 0
|
||||
for (const itemContentSize of itemContentSizes) {
|
||||
const contentSize = itemContentSize.contentSize ?? 0
|
||||
for (const itemContentSize of itemContentSizeDescriptors) {
|
||||
const contentSize = itemContentSize.props.contentSize ?? 0
|
||||
|
||||
itemUuidsToFetch.push(itemContentSize.uuid)
|
||||
itemUuidsToFetch.push(itemContentSize.props.uuid.value)
|
||||
totalContentSizeInBytes += contentSize
|
||||
|
||||
const transferLimitBreached = this.isTransferLimitBreached({
|
||||
totalContentSizeInBytes,
|
||||
bytesTransferLimit,
|
||||
itemUuidsToFetch,
|
||||
itemContentSizes,
|
||||
itemContentSizeDescriptors,
|
||||
})
|
||||
|
||||
if (transferLimitBreached) {
|
||||
@@ -32,22 +33,24 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
return itemUuidsToFetch
|
||||
}
|
||||
|
||||
async computeItemUuidBundlesToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<Array<string>>> {
|
||||
async computeItemUuidBundlesToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<Array<string>>> {
|
||||
let itemUuidsToFetch = []
|
||||
const itemContentSizes = await this.itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
|
||||
let totalContentSizeInBytes = 0
|
||||
const bundles = []
|
||||
for (const itemContentSize of itemContentSizes) {
|
||||
const contentSize = itemContentSize.contentSize ?? 0
|
||||
for (const itemContentSize of itemContentSizeDescriptors) {
|
||||
const contentSize = itemContentSize.props.contentSize ?? 0
|
||||
|
||||
itemUuidsToFetch.push(itemContentSize.uuid)
|
||||
itemUuidsToFetch.push(itemContentSize.props.uuid.value)
|
||||
totalContentSizeInBytes += contentSize
|
||||
|
||||
const transferLimitBreached = this.isTransferLimitBreached({
|
||||
totalContentSizeInBytes,
|
||||
bytesTransferLimit,
|
||||
itemUuidsToFetch,
|
||||
itemContentSizes,
|
||||
itemContentSizeDescriptors,
|
||||
})
|
||||
|
||||
if (transferLimitBreached) {
|
||||
@@ -68,11 +71,11 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
totalContentSizeInBytes: number
|
||||
bytesTransferLimit: number
|
||||
itemUuidsToFetch: Array<string>
|
||||
itemContentSizes: Array<{ uuid: string; contentSize: number | null }>
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[]
|
||||
}): boolean {
|
||||
const transferLimitBreached = dto.totalContentSizeInBytes >= dto.bytesTransferLimit
|
||||
const transferLimitBreachedAtFirstItem =
|
||||
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizes.length > 1
|
||||
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizeDescriptors.length > 1
|
||||
|
||||
if (transferLimitBreachedAtFirstItem) {
|
||||
this.logger.warn(
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export interface ItemTransferCalculatorInterface {
|
||||
computeItemUuidsToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<string>>
|
||||
computeItemUuidBundlesToFetch(itemQuery: ItemQuery, bytesTransferLimit: number): Promise<Array<Array<string>>>
|
||||
computeItemUuidsToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<string>>
|
||||
computeItemUuidBundlesToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
): Promise<Array<Array<string>>>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ContentType, RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
|
||||
import { CheckIntegrity } from './CheckIntegrity'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('CheckIntegrity', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
|
||||
const createUseCase = () => new CheckIntegrity(itemRepository)
|
||||
const createUseCase = () => new CheckIntegrity(itemRepositoryResolver)
|
||||
|
||||
beforeEach(() => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
@@ -40,6 +42,9 @@ describe('CheckIntegrity', () => {
|
||||
content_type: ContentType.TYPES.File,
|
||||
},
|
||||
])
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
})
|
||||
|
||||
it('should return an empty result if there are no integrity mismatches', async () => {
|
||||
@@ -63,6 +68,7 @@ describe('CheckIntegrity', () => {
|
||||
updated_at_timestamp: 5,
|
||||
},
|
||||
],
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.getValue()).toEqual([])
|
||||
})
|
||||
@@ -88,6 +94,7 @@ describe('CheckIntegrity', () => {
|
||||
updated_at_timestamp: 5,
|
||||
},
|
||||
],
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.getValue()).toEqual([
|
||||
{
|
||||
@@ -114,6 +121,7 @@ describe('CheckIntegrity', () => {
|
||||
updated_at_timestamp: 5,
|
||||
},
|
||||
],
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.getValue()).toEqual([
|
||||
{
|
||||
@@ -122,4 +130,27 @@ describe('CheckIntegrity', () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should return error if the role names are invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
integrityPayloads: [
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
updated_at_timestamp: 2,
|
||||
},
|
||||
{
|
||||
uuid: '5-6-7',
|
||||
updated_at_timestamp: 5,
|
||||
},
|
||||
],
|
||||
roleNames: ['invalid-role-name'],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Invalid role name: invalid-role-name')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { IntegrityPayload } from '@standardnotes/responses'
|
||||
import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ContentType, Result, RoleNameCollection, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { CheckIntegrityDTO } from './CheckIntegrityDTO'
|
||||
import { ExtendedIntegrityPayload } from '../../../Item/ExtendedIntegrityPayload'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class CheckIntegrity implements UseCaseInterface<IntegrityPayload[]> {
|
||||
constructor(private itemRepository: ItemRepositoryInterface) {}
|
||||
constructor(private itemRepositoryResolver: ItemRepositoryResolverInterface) {}
|
||||
|
||||
async execute(dto: CheckIntegrityDTO): Promise<Result<IntegrityPayload[]>> {
|
||||
const serverItemIntegrityPayloads = await this.itemRepository.findItemsForComputingIntegrityPayloads(dto.userUuid)
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
const serverItemIntegrityPayloads = await itemRepository.findItemsForComputingIntegrityPayloads(dto.userUuid)
|
||||
|
||||
const serverItemIntegrityPayloadsMap = new Map<string, ExtendedIntegrityPayload>()
|
||||
for (const serverItemIntegrityPayload of serverItemIntegrityPayloads) {
|
||||
|
||||
@@ -2,5 +2,6 @@ import { IntegrityPayload } from '@standardnotes/responses'
|
||||
|
||||
export type CheckIntegrityDTO = {
|
||||
userUuid: string
|
||||
roleNames: string[]
|
||||
integrityPayloads: IntegrityPayload[]
|
||||
}
|
||||
|
||||
@@ -3,26 +3,43 @@ import { Item } from '../../../Item/Item'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
|
||||
import { GetItem } from './GetItem'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
describe('GetItem', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
|
||||
const createUseCase = () => new GetItem(itemRepository)
|
||||
const createUseCase = () => new GetItem(itemRepositoryResolver)
|
||||
|
||||
beforeEach(() => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
})
|
||||
|
||||
it('should fail if item is not found', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemUuid: '2-3-4',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Could not find item with uuid 2-3-4')
|
||||
})
|
||||
|
||||
it('should fail if the role names are invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemUuid: '2-3-4',
|
||||
roleNames: ['invalid-role-name'],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Invalid role name: invalid-role-name')
|
||||
})
|
||||
|
||||
it('should succeed if item is found', async () => {
|
||||
const item = {} as jest.Mocked<Item>
|
||||
itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
|
||||
@@ -30,6 +47,7 @@ describe('GetItem', () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemUuid: '2-3-4',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.getValue()).toEqual(item)
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { Result, RoleNameCollection, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { GetItemDTO } from './GetItemDTO'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class GetItem implements UseCaseInterface<Item> {
|
||||
constructor(private itemRepository: ItemRepositoryInterface) {}
|
||||
constructor(private itemRepositoryResolver: ItemRepositoryResolverInterface) {}
|
||||
|
||||
async execute(dto: GetItemDTO): Promise<Result<Item>> {
|
||||
const item = await this.itemRepository.findByUuidAndUserUuid(dto.itemUuid, dto.userUuid)
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
const item = await itemRepository.findByUuidAndUserUuid(dto.itemUuid, dto.userUuid)
|
||||
|
||||
if (item === null) {
|
||||
return Result.fail(`Could not find item with uuid ${dto.itemUuid}`)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type GetItemDTO = {
|
||||
userUuid: string
|
||||
itemUuid: string
|
||||
roleNames: string[]
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalculatorInterface'
|
||||
import { GetItems } from './GetItems'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ContentType, Dates, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { ContentType, Dates, RoleName, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
import { ItemContentSizeDescriptor } from '../../../Item/ItemContentSizeDescriptor'
|
||||
|
||||
describe('GetItems', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
const contentSizeTransferLimit = 100
|
||||
let itemTransferCalculator: ItemTransferCalculatorInterface
|
||||
let timer: TimerInterface
|
||||
@@ -17,7 +20,7 @@ describe('GetItems', () => {
|
||||
|
||||
const createUseCase = () =>
|
||||
new GetItems(
|
||||
itemRepository,
|
||||
itemRepositoryResolver,
|
||||
sharedVaultUserRepository,
|
||||
contentSizeTransferLimit,
|
||||
itemTransferCalculator,
|
||||
@@ -43,6 +46,12 @@ describe('GetItems', () => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockResolvedValue([item])
|
||||
itemRepository.countAll = jest.fn().mockResolvedValue(1)
|
||||
itemRepository.findContentSizeForComputingTransferLimit = jest
|
||||
.fn()
|
||||
.mockResolvedValue([ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000000', 20).getValue()])
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockResolvedValue(['item-uuid'])
|
||||
@@ -60,6 +69,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: 10,
|
||||
@@ -80,6 +90,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
@@ -98,6 +109,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
cursorToken: 'MjowLjAwMDEyMw==',
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
@@ -119,6 +131,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
syncToken,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
@@ -140,6 +153,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
syncToken,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
@@ -154,6 +168,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: 200,
|
||||
@@ -172,6 +187,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
@@ -181,6 +197,21 @@ describe('GetItems', () => {
|
||||
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
|
||||
})
|
||||
|
||||
it('should return error for invalid role names', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['invalid'],
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Invalid role name: invalid')
|
||||
})
|
||||
|
||||
it('should filter shared vault uuids user wants to sync with the ones it has access to', async () => {
|
||||
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([
|
||||
{
|
||||
@@ -194,6 +225,7 @@ describe('GetItems', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Time, TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { GetItemsResult } from './GetItemsResult'
|
||||
import { ItemQuery } from '../../../Item/ItemQuery'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalculatorInterface'
|
||||
import { GetItemsDTO } from './GetItemsDTO'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class GetItems implements UseCaseInterface<GetItemsResult> {
|
||||
private readonly DEFAULT_ITEMS_LIMIT = 150
|
||||
private readonly SYNC_TOKEN_VERSION = 2
|
||||
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private contentSizeTransferLimit: number,
|
||||
private itemTransferCalculator: ItemTransferCalculatorInterface,
|
||||
@@ -35,6 +35,12 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const syncTimeComparison = dto.cursorToken ? '>=' : '>'
|
||||
const limit = dto.limit === undefined || dto.limit < 1 ? this.DEFAULT_ITEMS_LIMIT : dto.limit
|
||||
const upperBoundLimit = limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit
|
||||
@@ -59,19 +65,22 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
|
||||
exclusiveSharedVaultUuids,
|
||||
}
|
||||
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
|
||||
const itemContentSizeDescriptors = await itemRepository.findContentSizeForComputingTransferLimit(itemQuery)
|
||||
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
||||
itemQuery,
|
||||
itemContentSizeDescriptors,
|
||||
this.contentSizeTransferLimit,
|
||||
)
|
||||
let items: Array<Item> = []
|
||||
if (itemUuidsToFetch.length > 0) {
|
||||
items = await this.itemRepository.findAll({
|
||||
items = await itemRepository.findAll({
|
||||
uuids: itemUuidsToFetch,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
}
|
||||
const totalItemsCount = await this.itemRepository.countAll(itemQuery)
|
||||
const totalItemsCount = await itemRepository.countAll(itemQuery)
|
||||
|
||||
let cursorToken = undefined
|
||||
if (totalItemsCount > upperBoundLimit) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface GetItemsDTO {
|
||||
userUuid: string
|
||||
roleNames: string[]
|
||||
syncToken?: string | null
|
||||
cursorToken?: string | null
|
||||
limit?: number
|
||||
|
||||
@@ -5,13 +5,15 @@ import { SaveItems } from './SaveItems'
|
||||
import { SaveNewItem } from '../SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
|
||||
import { Logger } from 'winston'
|
||||
import { ContentType, Dates, Result, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { ContentType, Dates, Result, RoleName, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('SaveItems', () => {
|
||||
let itemSaveValidator: ItemSaveValidatorInterface
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let timer: TimerInterface
|
||||
let saveNewItem: SaveNewItem
|
||||
let updateExistingItem: UpdateExistingItem
|
||||
@@ -20,7 +22,7 @@ describe('SaveItems', () => {
|
||||
let savedItem: Item
|
||||
|
||||
const createUseCase = () =>
|
||||
new SaveItems(itemSaveValidator, itemRepository, timer, saveNewItem, updateExistingItem, logger)
|
||||
new SaveItems(itemSaveValidator, itemRepositoryResolver, timer, saveNewItem, updateExistingItem, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
|
||||
@@ -29,6 +31,9 @@ describe('SaveItems', () => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
|
||||
@@ -83,6 +88,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -91,6 +97,7 @@ describe('SaveItems', () => {
|
||||
itemHash: itemHash1,
|
||||
userUuid: 'user-uuid',
|
||||
sessionUuid: 'session-uuid',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -106,6 +113,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -129,6 +137,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -150,6 +159,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: true,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -172,6 +182,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -190,6 +201,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -208,6 +220,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -216,6 +229,7 @@ describe('SaveItems', () => {
|
||||
existingItem: savedItem,
|
||||
sessionUuid: 'session-uuid',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -232,6 +246,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -256,6 +271,7 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -301,9 +317,27 @@ describe('SaveItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().syncToken).toEqual('MjowLjAwMDE2')
|
||||
})
|
||||
|
||||
it('should fail if the role names are invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '2',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
snjsVersion: '2.200.0',
|
||||
roleNames: ['invalid-role-name'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Invalid role name: invalid-role-name')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SaveItemsResult } from './SaveItemsResult'
|
||||
import { SaveItemsDTO } from './SaveItemsDTO'
|
||||
@@ -7,17 +7,17 @@ import { ItemConflict } from '../../../Item/ItemConflict'
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
import { Time, TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemSaveValidatorInterface } from '../../../Item/SaveValidator/ItemSaveValidatorInterface'
|
||||
import { SaveNewItem } from '../SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class SaveItems implements UseCaseInterface<SaveItemsResult> {
|
||||
private readonly SYNC_TOKEN_VERSION = 2
|
||||
|
||||
constructor(
|
||||
private itemSaveValidator: ItemSaveValidatorInterface,
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private timer: TimerInterface,
|
||||
private saveNewItem: SaveNewItem,
|
||||
private updateExistingItem: UpdateExistingItem,
|
||||
@@ -28,6 +28,12 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
|
||||
const savedItems: Array<Item> = []
|
||||
const conflicts: Array<ItemConflict> = []
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
for (const itemHash of dto.itemHashes) {
|
||||
@@ -42,7 +48,8 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
|
||||
}
|
||||
const itemUuid = itemUuidOrError.getValue()
|
||||
|
||||
const existingItem = await this.itemRepository.findByUuid(itemUuid)
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
const existingItem = await itemRepository.findByUuid(itemUuid)
|
||||
|
||||
if (dto.readOnlyAccess) {
|
||||
conflicts.push({
|
||||
@@ -78,6 +85,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
performingUserUuid: dto.userUuid,
|
||||
roleNames: dto.roleNames,
|
||||
})
|
||||
if (udpatedItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
@@ -100,6 +108,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
|
||||
userUuid: dto.userUuid,
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
roleNames: dto.roleNames,
|
||||
})
|
||||
if (newItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
|
||||
@@ -7,4 +7,5 @@ export interface SaveItemsDTO {
|
||||
readOnlyAccess: boolean
|
||||
sessionUuid: string | null
|
||||
snjsVersion: string
|
||||
roleNames: string[]
|
||||
}
|
||||
|
||||
@@ -4,20 +4,22 @@ 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 { ContentType, Dates, Result, RoleName, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('SaveNewItem', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let timer: TimerInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let itemHash1: ItemHash
|
||||
let item1: Item
|
||||
|
||||
const createUseCase = () => new SaveNewItem(itemRepository, timer, domainEventPublisher, domainEventFactory)
|
||||
const createUseCase = () => new SaveNewItem(itemRepositoryResolver, timer, domainEventPublisher, domainEventFactory)
|
||||
|
||||
beforeEach(() => {
|
||||
const timeHelper = new Timer()
|
||||
@@ -62,6 +64,9 @@ describe('SaveNewItem', () => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.save = jest.fn()
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
|
||||
@@ -85,6 +90,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -106,6 +112,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -125,6 +132,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -142,6 +150,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -161,6 +170,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -180,6 +190,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -196,6 +207,20 @@ describe('SaveNewItem', () => {
|
||||
userUuid: '00000000-0000-0000-0000-00000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns a failure if the role names are invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
roleNames: ['invalid'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -206,6 +231,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-00000000000',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -223,6 +249,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -240,6 +267,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -257,6 +285,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -276,6 +305,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -297,6 +327,7 @@ describe('SaveNewItem', () => {
|
||||
userUuid: '00000000-0000-0000-0000-00000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -313,6 +344,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -339,6 +371,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -360,6 +393,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -378,6 +412,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
@@ -400,6 +435,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ContentType,
|
||||
Dates,
|
||||
Result,
|
||||
RoleNameCollection,
|
||||
Timestamps,
|
||||
UniqueEntityId,
|
||||
UseCaseInterface,
|
||||
@@ -13,14 +14,14 @@ 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'
|
||||
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
@@ -47,6 +48,12 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.props.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return Result.fail(contentTypeOrError.getError())
|
||||
@@ -135,7 +142,9 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
newItem.setKeySystemAssociation(keySystemAssociationOrError.getValue())
|
||||
}
|
||||
|
||||
await this.itemRepository.save(newItem)
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
|
||||
await itemRepository.save(newItem)
|
||||
|
||||
if (contentType.value !== null && [ContentType.TYPES.Note, ContentType.TYPES.File].includes(contentType.value)) {
|
||||
await this.domainEventPublisher.publish(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ItemHash } from '../../../Item/ItemHash'
|
||||
|
||||
export interface SaveNewItemDTO {
|
||||
userUuid: string
|
||||
roleNames: string[]
|
||||
itemHash: ItemHash
|
||||
sessionUuid: string | null
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Item } from '../../../Item/Item'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
|
||||
import { SyncItems } from './SyncItems'
|
||||
import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { ContentType, Dates, Result, RoleName, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { GetItems } from '../GetItems/GetItems'
|
||||
import { SaveItems } from '../SaveItems/SaveItems'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
@@ -13,11 +13,13 @@ import { GetSharedVaults } from '../../SharedVaults/GetSharedVaults/GetSharedVau
|
||||
import { GetMessagesSentToUser } from '../../Messaging/GetMessagesSentToUser/GetMessagesSentToUser'
|
||||
import { GetUserNotifications } from '../../Messaging/GetUserNotifications/GetUserNotifications'
|
||||
import { GetSharedVaultInvitesSentToUser } from '../../SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('SyncItems', () => {
|
||||
let getItemsUseCase: GetItems
|
||||
let saveItemsUseCase: SaveItems
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let item1: Item
|
||||
let item2: Item
|
||||
let item3: Item
|
||||
@@ -29,7 +31,7 @@ describe('SyncItems', () => {
|
||||
|
||||
const createUseCase = () =>
|
||||
new SyncItems(
|
||||
itemRepository,
|
||||
itemRepositoryResolver,
|
||||
getItemsUseCase,
|
||||
saveItemsUseCase,
|
||||
getSharedVaultsUseCase,
|
||||
@@ -122,6 +124,9 @@ describe('SyncItems', () => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item3, item1])
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
getSharedVaultsUseCase = {} as jest.Mocked<GetSharedVaults>
|
||||
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
|
||||
|
||||
@@ -148,6 +153,7 @@ describe('SyncItems', () => {
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
sessionUuid: null,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.getValue()).toEqual({
|
||||
conflicts: [],
|
||||
@@ -167,12 +173,14 @@ describe('SyncItems', () => {
|
||||
limit: 10,
|
||||
syncToken: 'foo',
|
||||
userUuid: '1-2-3',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
expect(saveItemsUseCase.execute).toHaveBeenCalledWith({
|
||||
itemHashes: [itemHash],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: '20200115',
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: null,
|
||||
})
|
||||
@@ -189,6 +197,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.getValue()).toEqual({
|
||||
conflicts: [],
|
||||
@@ -214,6 +223,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
sharedVaultUuids: ['00000000-0000-0000-0000-000000000000'],
|
||||
})
|
||||
expect(result.getValue()).toEqual({
|
||||
@@ -266,6 +276,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.getValue()).toEqual({
|
||||
@@ -305,6 +316,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -325,6 +337,24 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if role names are invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemHashes: [itemHash],
|
||||
computeIntegrityHash: false,
|
||||
limit: 10,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: ['invalid'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -345,6 +375,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -365,6 +396,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -385,6 +417,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -405,6 +438,7 @@ describe('SyncItems', () => {
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ContentType, Result, RoleNameCollection, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemConflict } from '../../../Item/ItemConflict'
|
||||
import { SyncItemsDTO } from './SyncItemsDTO'
|
||||
import { SyncItemsResponse } from './SyncItemsResponse'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { GetItems } from '../GetItems/GetItems'
|
||||
import { SaveItems } from '../SaveItems/SaveItems'
|
||||
import { GetSharedVaults } from '../../SharedVaults/GetSharedVaults/GetSharedVaults'
|
||||
import { GetSharedVaultInvitesSentToUser } from '../../SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
|
||||
import { GetMessagesSentToUser } from '../../Messaging/GetMessagesSentToUser/GetMessagesSentToUser'
|
||||
import { GetUserNotifications } from '../../Messaging/GetUserNotifications/GetUserNotifications'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private getItemsUseCase: GetItems,
|
||||
private saveItemsUseCase: SaveItems,
|
||||
private getSharedVaultsUseCase: GetSharedVaults,
|
||||
@@ -24,6 +24,12 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
) {}
|
||||
|
||||
async execute(dto: SyncItemsDTO): Promise<Result<SyncItemsResponse>> {
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const getItemsResultOrError = await this.getItemsUseCase.execute({
|
||||
userUuid: dto.userUuid,
|
||||
syncToken: dto.syncToken,
|
||||
@@ -31,6 +37,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
limit: dto.limit,
|
||||
contentType: dto.contentType,
|
||||
sharedVaultUuids: dto.sharedVaultUuids,
|
||||
roleNames: dto.roleNames,
|
||||
})
|
||||
if (getItemsResultOrError.isFailed()) {
|
||||
return Result.fail(getItemsResultOrError.getError())
|
||||
@@ -44,6 +51,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
readOnlyAccess: dto.readOnlyAccess,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
snjsVersion: dto.snjsVersion,
|
||||
roleNames: dto.roleNames,
|
||||
})
|
||||
if (saveItemsResultOrError.isFailed()) {
|
||||
return Result.fail(saveItemsResultOrError.getError())
|
||||
@@ -53,7 +61,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
let retrievedItems = this.filterOutSyncConflictsForConsecutiveSyncs(getItemsResult.items, saveItemsResult.conflicts)
|
||||
const isSharedVaultExclusiveSync = dto.sharedVaultUuids && dto.sharedVaultUuids.length > 0
|
||||
if (this.isFirstSync(dto) && !isSharedVaultExclusiveSync) {
|
||||
retrievedItems = await this.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems)
|
||||
retrievedItems = await this.frontLoadKeysItemsToTop(dto.userUuid, roleNames, retrievedItems)
|
||||
}
|
||||
|
||||
const sharedVaultsOrError = await this.getSharedVaultsUseCase.execute({
|
||||
@@ -125,8 +133,13 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
return retrievedItems.filter((item: Item) => syncConflictIds.indexOf(item.id.toString()) === -1)
|
||||
}
|
||||
|
||||
private async frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>> {
|
||||
const itemsKeys = await this.itemRepository.findAll({
|
||||
private async frontLoadKeysItemsToTop(
|
||||
userUuid: string,
|
||||
roleNames: RoleNameCollection,
|
||||
retrievedItems: Array<Item>,
|
||||
): Promise<Array<Item>> {
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
const itemsKeys = await itemRepository.findAll({
|
||||
userUuid,
|
||||
contentType: ContentType.TYPES.ItemsKey,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ItemHash } from '../../../Item/ItemHash'
|
||||
|
||||
export type SyncItemsDTO = {
|
||||
userUuid: string
|
||||
roleNames: string[]
|
||||
itemHashes: Array<ItemHash>
|
||||
computeIntegrityHash: boolean
|
||||
limit: number
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
UniqueEntityId,
|
||||
Result,
|
||||
NotificationPayload,
|
||||
RoleName,
|
||||
} from '@standardnotes/domain-core'
|
||||
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
|
||||
@@ -20,9 +21,11 @@ import { DetermineSharedVaultOperationOnItem } from '../../SharedVaults/Determin
|
||||
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
|
||||
import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
|
||||
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('UpdateExistingItem', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let timer: TimerInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
@@ -34,7 +37,7 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const createUseCase = () =>
|
||||
new UpdateExistingItem(
|
||||
itemRepository,
|
||||
itemRepositoryResolver,
|
||||
timer,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
@@ -88,6 +91,9 @@ describe('UpdateExistingItem', () => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.save = jest.fn()
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
|
||||
timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
|
||||
@@ -134,6 +140,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -148,6 +155,21 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: 'invalid-uuid',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if role names are invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: ['invalid-role'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -164,6 +186,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -180,6 +203,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -203,6 +227,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -221,6 +246,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -238,6 +264,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -256,6 +283,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -278,6 +306,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -302,6 +331,7 @@ describe('UpdateExistingItem', () => {
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
@@ -316,6 +346,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash: itemHash1,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: 'invalid-uuid',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -334,6 +365,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
|
||||
@@ -363,6 +395,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -389,6 +422,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
mock.mockRestore()
|
||||
@@ -409,6 +443,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -440,6 +475,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -474,6 +510,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
@@ -495,6 +532,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -514,6 +552,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(item1.props.keySystemAssociation).not.toBeUndefined()
|
||||
@@ -540,6 +579,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
@@ -561,6 +601,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
@@ -583,6 +624,7 @@ describe('UpdateExistingItem', () => {
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
roleNames: [RoleName.NAMES.CoreUser],
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
mock.mockRestore()
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
NotificationPayload,
|
||||
NotificationType,
|
||||
Result,
|
||||
RoleNameCollection,
|
||||
Timestamps,
|
||||
UniqueEntityId,
|
||||
UseCaseInterface,
|
||||
@@ -15,7 +16,6 @@ import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { UpdateExistingItemDTO } from './UpdateExistingItemDTO'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
|
||||
@@ -23,10 +23,11 @@ import { DetermineSharedVaultOperationOnItem } from '../../SharedVaults/Determin
|
||||
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
|
||||
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
|
||||
import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
|
||||
import { ItemRepositoryResolverInterface } from '../../../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
@@ -53,6 +54,12 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
if (dto.itemHash.props.content) {
|
||||
dto.existingItem.props.content = dto.itemHash.props.content
|
||||
}
|
||||
@@ -190,7 +197,9 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
dto.existingItem.props.itemsKeyId = null
|
||||
}
|
||||
|
||||
await this.itemRepository.save(dto.existingItem)
|
||||
const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
|
||||
|
||||
await itemRepository.save(dto.existingItem)
|
||||
|
||||
if (secondsFromLastUpdate >= this.revisionFrequency) {
|
||||
if (
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface UpdateExistingItemDTO {
|
||||
itemHash: ItemHash
|
||||
sessionUuid: string | null
|
||||
performingUserUuid: string
|
||||
roleNames: string[]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ControllerContainerInterface, MapperInterface, Validator } from '@standardnotes/domain-core'
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { Request, Response } from 'express'
|
||||
import { HttpStatusCode } from '@standardnotes/responses'
|
||||
import { Role } from '@standardnotes/security'
|
||||
|
||||
import { Item } from '../../../Domain/Item/Item'
|
||||
import { SyncResponseFactoryResolverInterface } from '../../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
|
||||
@@ -8,7 +10,6 @@ import { CheckIntegrity } from '../../../Domain/UseCase/Syncing/CheckIntegrity/C
|
||||
import { GetItem } from '../../../Domain/UseCase/Syncing/GetItem/GetItem'
|
||||
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'
|
||||
import { ItemHash } from '../../../Domain/Item/ItemHash'
|
||||
|
||||
@@ -59,6 +60,7 @@ export class BaseItemsController extends BaseHttpController {
|
||||
|
||||
const syncResult = await this.syncItems.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
roleNames: response.locals.roles.map((role: Role) => role.name),
|
||||
itemHashes,
|
||||
computeIntegrityHash: request.body.compute_integrity === true,
|
||||
syncToken: request.body.sync_token,
|
||||
@@ -91,6 +93,7 @@ export class BaseItemsController extends BaseHttpController {
|
||||
const result = await this.checkIntegrity.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
integrityPayloads,
|
||||
roleNames: response.locals.roles.map((role: Role) => role.name),
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -106,6 +109,7 @@ export class BaseItemsController extends BaseHttpController {
|
||||
const result = await this.getItem.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
itemUuid: request.params.uuid,
|
||||
roleNames: response.locals.roles.map((role: Role) => role.name),
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemQuery } from '../../Domain/Item/ItemQuery'
|
||||
import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
|
||||
import { MongoDBItem } from './MongoDBItem'
|
||||
import { ItemContentSizeDescriptor } from '../../Domain/Item/ItemContentSizeDescriptor'
|
||||
|
||||
export class MongoDBItemRepository implements ItemRepositoryInterface {
|
||||
constructor(
|
||||
@@ -44,23 +45,30 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
|
||||
return this.mongoRepository.count((options as FindManyOptions<MongoDBItem>).where)
|
||||
}
|
||||
|
||||
async findContentSizeForComputingTransferLimit(
|
||||
query: ItemQuery,
|
||||
): Promise<{ uuid: string; contentSize: number | null }[]> {
|
||||
async findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<ItemContentSizeDescriptor[]> {
|
||||
const options = this.createFindOptions(query)
|
||||
const rawItems = await this.mongoRepository.find({
|
||||
select: ['uuid', 'contentSize'],
|
||||
...options,
|
||||
})
|
||||
|
||||
const items = rawItems.map((item) => {
|
||||
return {
|
||||
uuid: item._id.toHexString(),
|
||||
contentSize: item.contentSize,
|
||||
}
|
||||
})
|
||||
const itemContentSizeDescriptors: ItemContentSizeDescriptor[] = []
|
||||
|
||||
return items
|
||||
for (const rawItem of rawItems) {
|
||||
const itemContentSizeDescriptorOrError = ItemContentSizeDescriptor.create(
|
||||
rawItem._id.toHexString(),
|
||||
rawItem.contentSize,
|
||||
)
|
||||
if (itemContentSizeDescriptorOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to create ItemContentSizeDescriptor for item ${rawItem._id.toHexString()}: ${itemContentSizeDescriptorOrError.getError()}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
itemContentSizeDescriptors.push(itemContentSizeDescriptorOrError.getValue())
|
||||
}
|
||||
|
||||
return itemContentSizeDescriptors
|
||||
}
|
||||
|
||||
async findDatesForComputingIntegrityHash(userUuid: string): Promise<{ updated_at_timestamp: number }[]> {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ItemQuery } from '../../Domain/Item/ItemQuery'
|
||||
import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
|
||||
import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
|
||||
import { TypeORMItem } from './TypeORMItem'
|
||||
import { ItemContentSizeDescriptor } from '../../Domain/Item/ItemContentSizeDescriptor'
|
||||
|
||||
export class TypeORMItemRepository implements ItemRepositoryInterface {
|
||||
constructor(
|
||||
@@ -38,16 +39,28 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
|
||||
.execute()
|
||||
}
|
||||
|
||||
async findContentSizeForComputingTransferLimit(
|
||||
query: ItemQuery,
|
||||
): Promise<{ uuid: string; contentSize: number | null }[]> {
|
||||
async findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<ItemContentSizeDescriptor[]> {
|
||||
const queryBuilder = this.createFindAllQueryBuilder(query)
|
||||
queryBuilder.select('item.uuid', 'uuid')
|
||||
queryBuilder.addSelect('item.content_size', 'contentSize')
|
||||
|
||||
const items = await queryBuilder.getRawMany()
|
||||
|
||||
return items
|
||||
const itemContentSizeDescriptors: ItemContentSizeDescriptor[] = []
|
||||
for (const item of items) {
|
||||
const ItemContentSizeDescriptorOrError = ItemContentSizeDescriptor.create(item.uuid, item.contentSize)
|
||||
if (ItemContentSizeDescriptorOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to create ItemContentSizeDescriptor for item ${
|
||||
item.uuid
|
||||
}: ${ItemContentSizeDescriptorOrError.getError()}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
itemContentSizeDescriptors.push(ItemContentSizeDescriptorOrError.getValue())
|
||||
}
|
||||
|
||||
return itemContentSizeDescriptors
|
||||
}
|
||||
|
||||
async deleteByUserUuid(userUuid: string): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { RoleName, RoleNameCollection } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
|
||||
import { ItemRepositoryResolverInterface } from '../../Domain/Item/ItemRepositoryResolverInterface'
|
||||
|
||||
export class TypeORMItemRepositoryResolver implements ItemRepositoryResolverInterface {
|
||||
constructor(
|
||||
private mysqlItemRepository: ItemRepositoryInterface,
|
||||
private mongoDbItemRepository: ItemRepositoryInterface | null,
|
||||
) {}
|
||||
|
||||
resolve(roleNames: RoleNameCollection): ItemRepositoryInterface {
|
||||
if (!this.mongoDbItemRepository) {
|
||||
return this.mysqlItemRepository
|
||||
}
|
||||
|
||||
const transitionRoleName = RoleName.create(RoleName.NAMES.TransitionUser).getValue()
|
||||
|
||||
if (roleNames.includes(transitionRoleName)) {
|
||||
return this.mongoDbItemRepository
|
||||
}
|
||||
|
||||
return this.mysqlItemRepository
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user