mirror of
https://github.com/standardnotes/server
synced 2026-01-18 08:04:28 -05:00
Compare commits
12 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d93916b159 | ||
|
|
c34f548e45 | ||
|
|
6fcd56cc86 | ||
|
|
8f88a87c93 | ||
|
|
f8c2f84322 | ||
|
|
46c4947871 | ||
|
|
64759ec2da | ||
|
|
5f7e768e64 | ||
|
|
4bc189f1c5 | ||
|
|
71721ab198 | ||
|
|
5536a48966 | ||
|
|
f77e29d3c9 |
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.25.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.3...@standardnotes/analytics@2.25.4) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.2...@standardnotes/analytics@2.25.3) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.3",
|
||||
"version": "2.25.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.67.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.2...@standardnotes/api-gateway@1.67.3) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.67.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.67.1...@standardnotes/api-gateway@1.67.2) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.67.2",
|
||||
"version": "1.67.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.126.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.2...@standardnotes/auth-server@1.126.3) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.126.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.126.1...@standardnotes/auth-server@1.126.2) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.126.2",
|
||||
"version": "1.126.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.23.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.23.3...@standardnotes/domain-core@1.23.4) (2023-07-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** persisting aggregate changes from root ([#674](https://github.com/standardnotes/server/issues/674)) ([c34f548](https://github.com/standardnotes/server/commit/c34f548e45bbd8defb8d490936e90755fd284e78))
|
||||
|
||||
## [1.23.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.23.2...@standardnotes/domain-core@1.23.3) (2023-07-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.23.3",
|
||||
"version": "1.23.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { Change } from './Change'
|
||||
import { Entity } from './Entity'
|
||||
|
||||
export abstract class Aggregate<T> extends Entity<T> {}
|
||||
export abstract class Aggregate<T> extends Entity<T> {
|
||||
private changesOnAggregateRoot: Change[] = []
|
||||
|
||||
addChange(change: Change): void {
|
||||
this.changesOnAggregateRoot.push(change)
|
||||
}
|
||||
|
||||
flushChanges(): void {
|
||||
this.changesOnAggregateRoot = []
|
||||
}
|
||||
|
||||
getChanges(): Change[] {
|
||||
return this.changesOnAggregateRoot
|
||||
}
|
||||
}
|
||||
|
||||
26
packages/domain-core/src/Domain/Core/Change.ts
Normal file
26
packages/domain-core/src/Domain/Core/Change.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { ChangeProps } from './ChangeProps'
|
||||
import { Result } from './Result'
|
||||
|
||||
export class Change {
|
||||
static readonly TYPES = {
|
||||
Add: 'add',
|
||||
Remove: 'remove',
|
||||
Modify: 'modify',
|
||||
}
|
||||
|
||||
public readonly props: ChangeProps
|
||||
|
||||
constructor(props: ChangeProps) {
|
||||
this.props = Object.freeze(props)
|
||||
}
|
||||
|
||||
static create(props: ChangeProps): Result<Change> {
|
||||
if (!Object.values(Change.TYPES).includes(props.changeType)) {
|
||||
return Result.fail('Invalid change type')
|
||||
}
|
||||
|
||||
return Result.ok(new Change(props))
|
||||
}
|
||||
}
|
||||
9
packages/domain-core/src/Domain/Core/ChangeProps.ts
Normal file
9
packages/domain-core/src/Domain/Core/ChangeProps.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { Entity } from './Entity'
|
||||
|
||||
export interface ChangeProps {
|
||||
aggregateRootUuid: string
|
||||
changeType: string
|
||||
changeData: Entity<unknown>
|
||||
}
|
||||
@@ -27,6 +27,8 @@ export * from './Common/Uuid'
|
||||
export * from './Common/UuidProps'
|
||||
|
||||
export * from './Core/Aggregate'
|
||||
export * from './Core/Change'
|
||||
export * from './Core/ChangeProps'
|
||||
export * from './Core/Entity'
|
||||
export * from './Core/Id'
|
||||
export * from './Core/Result'
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.11](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.10...@standardnotes/event-store@1.11.11) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.10](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.9...@standardnotes/event-store@1.11.10) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.10",
|
||||
"version": "1.11.11",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.19.13](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.12...@standardnotes/files-server@1.19.13) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.19.12](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.11...@standardnotes/files-server@1.19.12) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.19.12",
|
||||
"version": "1.19.13",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.11](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.10...@standardnotes/home-server@1.13.11) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.13.10](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.9...@standardnotes/home-server@1.13.10) (2023-07-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.13.9](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.8...@standardnotes/home-server@1.13.9) (2023-07-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.13.8](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.7...@standardnotes/home-server@1.13.8) (2023-07-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.13.7](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.6...@standardnotes/home-server@1.13.7) (2023-07-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.13.6](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.5...@standardnotes/home-server@1.13.6) (2023-07-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.13.5](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.4...@standardnotes/home-server@1.13.5) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.13.5",
|
||||
"version": "1.13.11",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.25.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.25.3...@standardnotes/revisions-server@1.25.4) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.25.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.25.2...@standardnotes/revisions-server@1.25.3) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.25.3",
|
||||
"version": "1.25.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.13](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.12...@standardnotes/scheduler-server@1.20.13) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.12](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.11...@standardnotes/scheduler-server@1.20.12) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.12",
|
||||
"version": "1.20.13",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.18](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.17...@standardnotes/settings@1.21.18) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.17](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.16...@standardnotes/settings@1.21.17) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/settings",
|
||||
"version": "1.21.17",
|
||||
"version": "1.21.18",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.70.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.70.2...@standardnotes/syncing-server@1.70.3) (2023-07-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** persisting aggregate changes from root ([#674](https://github.com/standardnotes/syncing-server-js/issues/674)) ([c34f548](https://github.com/standardnotes/syncing-server-js/commit/c34f548e45bbd8defb8d490936e90755fd284e78))
|
||||
|
||||
## [1.70.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.70.1...@standardnotes/syncing-server@1.70.2) (2023-07-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** remove notifications after adding item to vault ([#672](https://github.com/standardnotes/syncing-server-js/issues/672)) ([8f88a87](https://github.com/standardnotes/syncing-server-js/commit/8f88a87c93e21f52a029167f2408ff061e2a4e93))
|
||||
|
||||
## [1.70.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.70.0...@standardnotes/syncing-server@1.70.1) (2023-07-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** allow sender to decline the invite ([#671](https://github.com/standardnotes/syncing-server-js/issues/671)) ([46c4947](https://github.com/standardnotes/syncing-server-js/commit/46c4947871f342f0a07c68562b0e3e77e7e114d4))
|
||||
|
||||
# [1.70.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.69.0...@standardnotes/syncing-server@1.70.0) (2023-07-25)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** filtering items by shared vault permissions ([#670](https://github.com/standardnotes/syncing-server-js/issues/670)) ([5f7e768](https://github.com/standardnotes/syncing-server-js/commit/5f7e768e64da0452e6efcf70e36cb5e867291456))
|
||||
|
||||
# [1.69.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.4...@standardnotes/syncing-server@1.69.0) (2023-07-24)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** determin shared vault operation type ([#669](https://github.com/standardnotes/syncing-server-js/issues/669)) ([71721ab](https://github.com/standardnotes/syncing-server-js/commit/71721ab1982b65feb4c84b44b267a249b573c537))
|
||||
|
||||
## [1.68.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.3...@standardnotes/syncing-server@1.68.4) (2023-07-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** force remove shared vault owner when removing shared vault ([f77e29d](https://github.com/standardnotes/syncing-server-js/commit/f77e29d3c9c9a28be3c5624d2c9bf0ffd6348377))
|
||||
|
||||
## [1.68.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.68.2...@standardnotes/syncing-server@1.68.3) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.68.3",
|
||||
"version": "1.70.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -150,6 +150,9 @@ import { MessageHttpMapper } from '../Mapping/Http/MessageHttpMapper'
|
||||
import { GetUserNotifications } from '../Domain/UseCase/Messaging/GetUserNotifications/GetUserNotifications'
|
||||
import { NotificationHttpMapper } from '../Mapping/Http/NotificationHttpMapper'
|
||||
import { NotificationHttpRepresentation } from '../Mapping/Http/NotificationHttpRepresentation'
|
||||
import { DetermineSharedVaultOperationOnItem } from '../Domain/UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
|
||||
import { SharedVaultFilter } from '../Domain/Item/SaveRule/SharedVaultFilter'
|
||||
import { RemoveNotificationsForUser } from '../Domain/UseCase/Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -494,12 +497,24 @@ export class ContainerConfigLoader {
|
||||
.bind<TokenEncoderInterface<SharedVaultValetTokenData>>(TYPES.Sync_SharedVaultValetTokenEncoder)
|
||||
.toConstantValue(new TokenEncoder<SharedVaultValetTokenData>(container.get(TYPES.Sync_VALET_TOKEN_SECRET)))
|
||||
|
||||
container
|
||||
.bind<DetermineSharedVaultOperationOnItem>(TYPES.Sync_DetermineSharedVaultOperationOnItem)
|
||||
.toConstantValue(new DetermineSharedVaultOperationOnItem())
|
||||
|
||||
container.bind<OwnershipFilter>(TYPES.Sync_OwnershipFilter).toConstantValue(new OwnershipFilter())
|
||||
container
|
||||
.bind<TimeDifferenceFilter>(TYPES.Sync_TimeDifferenceFilter)
|
||||
.toConstantValue(new TimeDifferenceFilter(container.get(TYPES.Sync_Timer)))
|
||||
container.bind<ContentTypeFilter>(TYPES.Sync_ContentTypeFilter).toConstantValue(new ContentTypeFilter())
|
||||
container.bind<ContentFilter>(TYPES.Sync_ContentFilter).toConstantValue(new ContentFilter())
|
||||
container
|
||||
.bind<SharedVaultFilter>(TYPES.Sync_SharedVaultFilter)
|
||||
.toConstantValue(
|
||||
new SharedVaultFilter(
|
||||
container.get(TYPES.Sync_DetermineSharedVaultOperationOnItem),
|
||||
container.get(TYPES.Sync_SharedVaultUserRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
|
||||
.toConstantValue(
|
||||
@@ -508,6 +523,7 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_TimeDifferenceFilter),
|
||||
container.get(TYPES.Sync_ContentTypeFilter),
|
||||
container.get(TYPES.Sync_ContentFilter),
|
||||
container.get(TYPES.Sync_SharedVaultFilter),
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -534,6 +550,14 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser)
|
||||
.toConstantValue(
|
||||
new AddNotificationForUser(container.get(TYPES.Sync_NotificationRepository), container.get(TYPES.Sync_Timer)),
|
||||
)
|
||||
container
|
||||
.bind<RemoveNotificationsForUser>(TYPES.Sync_RemoveNotificationsForUser)
|
||||
.toConstantValue(new RemoveNotificationsForUser(container.get(TYPES.Sync_NotificationRepository)))
|
||||
container
|
||||
.bind<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem)
|
||||
.toConstantValue(
|
||||
@@ -543,6 +567,9 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_DomainEventPublisher),
|
||||
container.get(TYPES.Sync_DomainEventFactory),
|
||||
container.get(TYPES.Sync_REVISIONS_FREQUENCY),
|
||||
container.get(TYPES.Sync_DetermineSharedVaultOperationOnItem),
|
||||
container.get(TYPES.Sync_AddNotificationForUser),
|
||||
container.get(TYPES.Sync_RemoveNotificationsForUser),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -658,11 +685,6 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_SharedVaultRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser)
|
||||
.toConstantValue(
|
||||
new AddNotificationForUser(container.get(TYPES.Sync_NotificationRepository), container.get(TYPES.Sync_Timer)),
|
||||
)
|
||||
container
|
||||
.bind<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser)
|
||||
.toConstantValue(
|
||||
|
||||
@@ -57,6 +57,7 @@ const TYPES = {
|
||||
Sync_GetSharedVaultUsers: Symbol.for('Sync_GetSharedVaultUsers'),
|
||||
Sync_AddUserToSharedVault: Symbol.for('Sync_AddUserToSharedVault'),
|
||||
Sync_AddNotificationForUser: Symbol.for('Sync_AddNotificationForUser'),
|
||||
Sync_RemoveNotificationsForUser: Symbol.for('Sync_RemoveNotificationsForUser'),
|
||||
Sync_RemoveSharedVaultUser: Symbol.for('Sync_RemoveSharedVaultUser'),
|
||||
Sync_InviteUserToSharedVault: Symbol.for('Sync_InviteUserToSharedVault'),
|
||||
Sync_UpdateSharedVaultInvite: Symbol.for('Sync_UpdateSharedVaultInvite'),
|
||||
@@ -76,6 +77,7 @@ const TYPES = {
|
||||
Sync_GetItems: Symbol.for('Sync_GetItems'),
|
||||
Sync_SaveItems: Symbol.for('Sync_SaveItems'),
|
||||
Sync_GetUserNotifications: Symbol.for('Sync_GetUserNotifications'),
|
||||
Sync_DetermineSharedVaultOperationOnItem: Symbol.for('Sync_DetermineSharedVaultOperationOnItem'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
@@ -98,6 +100,7 @@ const TYPES = {
|
||||
Sync_ItemBackupService: Symbol.for('Sync_ItemBackupService'),
|
||||
Sync_ItemSaveValidator: Symbol.for('Sync_ItemSaveValidator'),
|
||||
Sync_OwnershipFilter: Symbol.for('Sync_OwnershipFilter'),
|
||||
Sync_SharedVaultFilter: Symbol.for('Sync_SharedVaultFilter'),
|
||||
Sync_TimeDifferenceFilter: Symbol.for('Sync_TimeDifferenceFilter'),
|
||||
Sync_ContentTypeFilter: Symbol.for('Sync_ContentTypeFilter'),
|
||||
Sync_ContentFilter: Symbol.for('Sync_ContentFilter'),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from './Item'
|
||||
import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation'
|
||||
|
||||
describe('Item', () => {
|
||||
it('should create an aggregate', () => {
|
||||
@@ -44,4 +46,207 @@ describe('Item', () => {
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(() => entityOrError.getValue().uuid).toThrow()
|
||||
})
|
||||
|
||||
it('should tell if an item is associated with a shared vault', () => {
|
||||
const entityOrError = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(
|
||||
entityOrError
|
||||
.getValue()
|
||||
.isAssociatedWithSharedVault(Uuid.create('00000000-0000-0000-0000-000000000000').getValue()),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell that an item is not associated with a shared vault', () => {
|
||||
const entityOrError = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(
|
||||
entityOrError
|
||||
.getValue()
|
||||
.isAssociatedWithSharedVault(Uuid.create('00000000-0000-0000-0000-000000000000').getValue()),
|
||||
).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should tell if an item is associated with a key system', () => {
|
||||
const entityOrError = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
keySystemAssociation: KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().isAssociatedWithKeySystem('key-system-identifier')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should tell that an item is not associated with a key system', () => {
|
||||
const entityOrError = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().isAssociatedWithKeySystem('key-system-identifier')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should set shared vault association', () => {
|
||||
const sharedVaultAssociation = SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
entity.setSharedVaultAssociation(sharedVaultAssociation)
|
||||
|
||||
expect(entity.props.sharedVaultAssociation).toEqual(sharedVaultAssociation)
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should unset a shared vault association', () => {
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
}).getValue()
|
||||
|
||||
entity.unsetSharedVaultAssociation()
|
||||
|
||||
expect(entity.props.sharedVaultAssociation).toBeUndefined()
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should set key system association', () => {
|
||||
const keySystemAssociation = KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
entity.setKeySystemAssociation(keySystemAssociation)
|
||||
|
||||
expect(entity.props.keySystemAssociation).toEqual(keySystemAssociation)
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should unset a key system association', () => {
|
||||
const entity = Item.create({
|
||||
duplicateOf: null,
|
||||
itemsKeyId: 'items-key-id',
|
||||
content: 'content',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: 'enc-item-key',
|
||||
authHash: 'auth-hash',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
deleted: false,
|
||||
updatedWithSession: null,
|
||||
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
keySystemAssociation: KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
}).getValue()
|
||||
|
||||
entity.unsetKeySystemAssociation()
|
||||
|
||||
expect(entity.props.keySystemAssociation).toBeUndefined()
|
||||
expect(entity.getChanges()).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { Aggregate, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { Aggregate, Change, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemProps } from './ItemProps'
|
||||
import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation'
|
||||
|
||||
export class Item extends Aggregate<ItemProps> {
|
||||
get uuid(): Uuid {
|
||||
const uuidOrError = Uuid.create(this._id.toString())
|
||||
if (uuidOrError.isFailed()) {
|
||||
throw new Error(uuidOrError.getError())
|
||||
}
|
||||
|
||||
return uuidOrError.getValue()
|
||||
}
|
||||
|
||||
private constructor(props: ItemProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
@@ -24,4 +17,95 @@ export class Item extends Aggregate<ItemProps> {
|
||||
|
||||
return Result.ok<Item>(new Item(props, id))
|
||||
}
|
||||
|
||||
get uuid(): Uuid {
|
||||
const uuidOrError = Uuid.create(this._id.toString())
|
||||
if (uuidOrError.isFailed()) {
|
||||
throw new Error(uuidOrError.getError())
|
||||
}
|
||||
|
||||
return uuidOrError.getValue()
|
||||
}
|
||||
|
||||
get sharedVaultUuid(): Uuid | null {
|
||||
if (!this.props.sharedVaultAssociation) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.props.sharedVaultAssociation.props.sharedVaultUuid
|
||||
}
|
||||
|
||||
isAssociatedWithASharedVault(): boolean {
|
||||
return this.sharedVaultUuid !== null
|
||||
}
|
||||
|
||||
isAssociatedWithSharedVault(sharedVaultUuid: Uuid): boolean {
|
||||
if (!this.isAssociatedWithASharedVault()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (this.sharedVaultUuid as Uuid).equals(sharedVaultUuid)
|
||||
}
|
||||
|
||||
isAssociatedWithKeySystem(keySystemIdentifier: string): boolean {
|
||||
if (!this.props.keySystemAssociation) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.props.keySystemAssociation.props.keySystemIdentifier === keySystemIdentifier
|
||||
}
|
||||
|
||||
setSharedVaultAssociation(sharedVaultAssociation: SharedVaultAssociation): void {
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: this.props.sharedVaultAssociation ? Change.TYPES.Modify : Change.TYPES.Add,
|
||||
changeData: sharedVaultAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
this.props.sharedVaultAssociation = sharedVaultAssociation
|
||||
}
|
||||
|
||||
unsetSharedVaultAssociation(): void {
|
||||
if (!this.props.sharedVaultAssociation) {
|
||||
return
|
||||
}
|
||||
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: Change.TYPES.Remove,
|
||||
changeData: this.props.sharedVaultAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
this.props.sharedVaultAssociation = undefined
|
||||
}
|
||||
|
||||
setKeySystemAssociation(keySystemAssociation: KeySystemAssociation): void {
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: this.props.keySystemAssociation ? Change.TYPES.Modify : Change.TYPES.Add,
|
||||
changeData: keySystemAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
this.props.keySystemAssociation = keySystemAssociation
|
||||
}
|
||||
|
||||
unsetKeySystemAssociation(): void {
|
||||
if (!this.props.keySystemAssociation) {
|
||||
return
|
||||
}
|
||||
|
||||
this.addChange(
|
||||
Change.create({
|
||||
aggregateRootUuid: this.uuid.value,
|
||||
changeType: Change.TYPES.Remove,
|
||||
changeData: this.props.keySystemAssociation,
|
||||
}).getValue(),
|
||||
)
|
||||
this.props.keySystemAssociation = undefined
|
||||
}
|
||||
}
|
||||
|
||||
38
packages/syncing-server/src/Domain/Item/ItemHash.spec.ts
Normal file
38
packages/syncing-server/src/Domain/Item/ItemHash.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ItemHash } from './ItemHash'
|
||||
|
||||
describe('ItemHash', () => {
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = ItemHash.create({
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
})
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return error if shared vault uuid is not valid', () => {
|
||||
const valueOrError = ItemHash.create({
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||
import { Result, Uuid, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemHashProps } from './ItemHashProps'
|
||||
|
||||
@@ -8,6 +8,13 @@ export class ItemHash extends ValueObject<ItemHashProps> {
|
||||
}
|
||||
|
||||
static create(props: ItemHashProps): Result<ItemHash> {
|
||||
if (props.shared_vault_uuid) {
|
||||
const sharedVaultUuidOrError = Uuid.create(props.shared_vault_uuid)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail<ItemHash>(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok<ItemHash>(new ItemHash(props))
|
||||
}
|
||||
|
||||
@@ -15,6 +22,14 @@ export class ItemHash extends ValueObject<ItemHashProps> {
|
||||
return this.props.shared_vault_uuid !== null
|
||||
}
|
||||
|
||||
get sharedVaultUuid(): Uuid | null {
|
||||
if (!this.representsASharedVaultItem()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Uuid.create(this.props.shared_vault_uuid as string).getValue()
|
||||
}
|
||||
|
||||
hasDedicatedKeySystemAssociation(): boolean {
|
||||
return this.props.key_system_identifier !== null
|
||||
}
|
||||
|
||||
@@ -59,6 +59,31 @@ describe('OwnershipFilter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should deffer to the shared vault filter if the item hash represents a shared vault item or existing item is a shared vault item', async () => {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000001',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should leave items belonging to the same user', async () => {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
|
||||
@@ -6,6 +6,14 @@ import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export class OwnershipFilter implements ItemSaveRuleInterface {
|
||||
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
|
||||
const deferToSharedVaultFilter =
|
||||
dto.existingItem?.isAssociatedWithASharedVault() || dto.itemHash.representsASharedVaultItem()
|
||||
if (deferToSharedVaultFilter) {
|
||||
return {
|
||||
passed: true,
|
||||
}
|
||||
}
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,812 @@
|
||||
import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
|
||||
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { DetermineSharedVaultOperationOnItem } from '../../UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
|
||||
import { SharedVaultFilter } from './SharedVaultFilter'
|
||||
import { ItemHash } from '../ItemHash'
|
||||
import { Item } from '../Item'
|
||||
import { SharedVaultOperationOnItem } from '../../SharedVault/SharedVaultOperationOnItem'
|
||||
import { SharedVaultAssociation } from '../../SharedVault/SharedVaultAssociation'
|
||||
|
||||
describe('SharedVaultFilter', () => {
|
||||
let determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
let sharedVaultUser: SharedVaultUser
|
||||
let itemHash: ItemHash
|
||||
let existingItem: Item
|
||||
|
||||
const createFilter = () => new SharedVaultFilter(determineSharedVaultOperationOnItem, sharedVaultUserRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
existingItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: 'key-system-identifier',
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(sharedVaultUser)
|
||||
.mockResolvedValueOnce(null)
|
||||
})
|
||||
|
||||
it('should return as passed if the item hash does not represent a shared vault item and existing item is not a shared vault item', async () => {
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
existingItem = Item.create({
|
||||
...existingItem.props,
|
||||
sharedVaultAssociation: undefined,
|
||||
}).getValue()
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(true)
|
||||
})
|
||||
|
||||
it('should return as not passed if the operation could not be determined', async () => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(Result.fail('error'))
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the item is a shared vault item without a dedicated key system association', async () => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
describe('when the shared vault operation on item is: move to other shared vault', () => {
|
||||
beforeEach(() => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(sharedVaultUser)
|
||||
.mockResolvedValueOnce(sharedVaultUser)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not a member of the target shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
|
||||
.fn()
|
||||
.mockResolvedValue(sharedVaultUser)
|
||||
.mockResolvedValue(null)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as passed if the user is a member of both shared vaults', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(true)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not a member of the target shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(sharedVaultUser)
|
||||
.mockReturnValueOnce(null)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the item is deleted', async () => {
|
||||
existingItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: true,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the item is being deleted', async () => {
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
deleted: true,
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
content_type: ContentType.TYPES.KeySystemItemsKey,
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the shared vault operation on item is: add to shared vault', () => {
|
||||
beforeEach(() => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as passed if the user is a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(true)
|
||||
})
|
||||
|
||||
it('should return as not passed if the item is deleted', async () => {
|
||||
existingItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: true,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not the owner of the item', async () => {
|
||||
existingItem = Item.create({
|
||||
...existingItem.props,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000001',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
content_type: ContentType.TYPES.KeySystemItemsKey,
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the shared vault operation on item is: remove from shared vault', () => {
|
||||
beforeEach(() => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as passed if the user is a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(true)
|
||||
})
|
||||
|
||||
it('should return as not passed if the item is deleted', async () => {
|
||||
existingItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: true,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not the owner of the item', async () => {
|
||||
existingItem = Item.create({
|
||||
...existingItem.props,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000001',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
content_type: ContentType.TYPES.KeySystemItemsKey,
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the shared vault operation on item is: save to shared vault', () => {
|
||||
beforeEach(() => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.SaveToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as passed if the user is a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(true)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user has insufficient permissions', async () => {
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the shared vault operation on item is: create to shared vault', () => {
|
||||
beforeEach(() => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.CreateToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
existingItem,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user is not a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
|
||||
it('should return as passed if the user is a member of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(true)
|
||||
})
|
||||
|
||||
it('should return as not passed if the user has insufficient permissions to write key system items key', async () => {
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
content_type: ContentType.TYPES.KeySystemItemsKey,
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.CreateToSharedVault,
|
||||
incomingItemHash: itemHash,
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
const filter = createFilter()
|
||||
const result = await filter.check({
|
||||
apiVersion: '001',
|
||||
existingItem: existingItem,
|
||||
itemHash: itemHash,
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.passed).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,231 @@
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
import { ContentType, Result, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
|
||||
import { ItemSaveRuleResult } from './ItemSaveRuleResult'
|
||||
import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
|
||||
import { DetermineSharedVaultOperationOnItem } from '../../UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
|
||||
import { SharedVaultOperationOnItem } from '../../SharedVault/SharedVaultOperationOnItem'
|
||||
import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
|
||||
export class SharedVaultFilter implements ItemSaveRuleInterface {
|
||||
constructor(
|
||||
private determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem,
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
|
||||
if (!dto.itemHash.representsASharedVaultItem() && !dto.existingItem?.isAssociatedWithASharedVault()) {
|
||||
return {
|
||||
passed: true,
|
||||
}
|
||||
}
|
||||
|
||||
const operationOrError = await this.determineSharedVaultOperationOnItem.execute({
|
||||
userUuid: dto.userUuid,
|
||||
itemHash: dto.itemHash,
|
||||
existingItem: dto.existingItem,
|
||||
})
|
||||
if (operationOrError.isFailed()) {
|
||||
return {
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: dto.itemHash,
|
||||
type: ConflictType.SharedVaultInvalidState,
|
||||
},
|
||||
}
|
||||
}
|
||||
const operation = operationOrError.getValue()
|
||||
|
||||
if (dto.itemHash.representsASharedVaultItem() && !dto.itemHash.hasDedicatedKeySystemAssociation()) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
|
||||
}
|
||||
|
||||
const sharedVaultPermission = await this.getSharedVaultUserPermission(
|
||||
operation.props.userUuid,
|
||||
operation.props.sharedVaultUuid,
|
||||
)
|
||||
|
||||
if (!sharedVaultPermission) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultNotMemberError)
|
||||
}
|
||||
|
||||
let targetSharedVaultPermission: SharedVaultUserPermission | null = null
|
||||
if (operation.props.targetSharedVaultUuid) {
|
||||
targetSharedVaultPermission = await this.getSharedVaultUserPermission(
|
||||
operation.props.userUuid,
|
||||
operation.props.targetSharedVaultUuid,
|
||||
)
|
||||
|
||||
if (!targetSharedVaultPermission) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultNotMemberError)
|
||||
}
|
||||
}
|
||||
|
||||
const resultOrError = await this.getResultForOperation(
|
||||
operation,
|
||||
sharedVaultPermission,
|
||||
targetSharedVaultPermission,
|
||||
)
|
||||
/* istanbul ignore next */
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
|
||||
}
|
||||
|
||||
return resultOrError.getValue()
|
||||
}
|
||||
|
||||
private async getResultForOperation(
|
||||
operation: SharedVaultOperationOnItem,
|
||||
sharedVaultPermission: SharedVaultUserPermission,
|
||||
targetSharedVaultPermission: SharedVaultUserPermission | null,
|
||||
): Promise<Result<ItemSaveRuleResult>> {
|
||||
switch (operation.props.type) {
|
||||
case SharedVaultOperationOnItem.TYPES.AddToSharedVault:
|
||||
case SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault:
|
||||
return Result.ok(await this.handleAddOrRemoveToSharedVaultOperation(operation, sharedVaultPermission))
|
||||
case SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault:
|
||||
return Result.ok(
|
||||
await this.handleMoveToOtherSharedVaultOperation(
|
||||
operation,
|
||||
sharedVaultPermission,
|
||||
targetSharedVaultPermission as SharedVaultUserPermission,
|
||||
),
|
||||
)
|
||||
case SharedVaultOperationOnItem.TYPES.SaveToSharedVault:
|
||||
case SharedVaultOperationOnItem.TYPES.CreateToSharedVault:
|
||||
return Result.ok(await this.handleSaveOrCreateToSharedVaultOperation(operation, sharedVaultPermission))
|
||||
/* istanbul ignore next */
|
||||
default:
|
||||
return Result.fail(`Unsupported sharedVault operation: ${operation}`)
|
||||
}
|
||||
}
|
||||
|
||||
private isAuthorizedToSaveContentType(contentType: string | null, permission: SharedVaultUserPermission): boolean {
|
||||
if (contentType === ContentType.TYPES.KeySystemItemsKey) {
|
||||
return permission.value === SharedVaultUserPermission.PERMISSIONS.Admin
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private async handleAddOrRemoveToSharedVaultOperation(
|
||||
operation: SharedVaultOperationOnItem,
|
||||
sharedVaultPermission: SharedVaultUserPermission,
|
||||
): Promise<ItemSaveRuleResult> {
|
||||
if (this.isItemDeletedOrBeingDeleted(operation)) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
|
||||
}
|
||||
|
||||
if (!this.isOwnerOfTheItem(operation)) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
|
||||
}
|
||||
|
||||
if (!this.hasSufficientPermissionsToWriteInVault(operation, sharedVaultPermission)) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
|
||||
}
|
||||
|
||||
return this.buildSuccessValue()
|
||||
}
|
||||
|
||||
private async handleMoveToOtherSharedVaultOperation(
|
||||
operation: SharedVaultOperationOnItem,
|
||||
sourceSharedVaultPermission: SharedVaultUserPermission,
|
||||
targetSharedVaultPermission: SharedVaultUserPermission,
|
||||
): Promise<ItemSaveRuleResult> {
|
||||
if (this.isItemDeletedOrBeingDeleted(operation)) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInvalidState)
|
||||
}
|
||||
|
||||
for (const permission of [sourceSharedVaultPermission, targetSharedVaultPermission]) {
|
||||
if (!this.hasSufficientPermissionsToWriteInVault(operation, permission)) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
|
||||
}
|
||||
}
|
||||
|
||||
return this.buildSuccessValue()
|
||||
}
|
||||
|
||||
private async handleSaveOrCreateToSharedVaultOperation(
|
||||
operation: SharedVaultOperationOnItem,
|
||||
sharedVaultPermission: SharedVaultUserPermission,
|
||||
): Promise<ItemSaveRuleResult> {
|
||||
if (!this.hasSufficientPermissionsToWriteInVault(operation, sharedVaultPermission)) {
|
||||
return this.buildFailResult(operation, ConflictType.SharedVaultInsufficientPermissionsError)
|
||||
}
|
||||
|
||||
return this.buildSuccessValue()
|
||||
}
|
||||
|
||||
private isItemDeletedOrBeingDeleted(operation: SharedVaultOperationOnItem): boolean {
|
||||
if (operation.props.existingItem?.props.deleted || operation.props.incomingItemHash.props.deleted) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private isOwnerOfTheItem(operation: SharedVaultOperationOnItem): boolean {
|
||||
if (operation.props.userUuid.equals(operation.props.existingItem?.props.userUuid)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private hasSufficientPermissionsToWriteInVault(
|
||||
operation: SharedVaultOperationOnItem,
|
||||
sharedVaultPermission: SharedVaultUserPermission,
|
||||
): boolean {
|
||||
if (
|
||||
!this.isAuthorizedToSaveContentType(operation.props.incomingItemHash.props.content_type, sharedVaultPermission)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (sharedVaultPermission.value === SharedVaultUserPermission.PERMISSIONS.Read) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private async getSharedVaultUserPermission(
|
||||
userUuid: Uuid,
|
||||
sharedVaultUuid: Uuid,
|
||||
): Promise<SharedVaultUserPermission | null> {
|
||||
const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
|
||||
userUuid,
|
||||
sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (sharedVaultUser) {
|
||||
return sharedVaultUser.props.permission
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private buildFailResult(operation: SharedVaultOperationOnItem, type: ConflictType): ItemSaveRuleResult {
|
||||
const includeServerItem = [
|
||||
ConflictType.SharedVaultInvalidState,
|
||||
ConflictType.SharedVaultInsufficientPermissionsError,
|
||||
].includes(type)
|
||||
|
||||
return {
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: operation.props.incomingItemHash,
|
||||
serverItem: includeServerItem ? operation.props.existingItem : undefined,
|
||||
type,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private buildSuccessValue(): ItemSaveRuleResult {
|
||||
return {
|
||||
passed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { NotificationType, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Notification } from './Notification'
|
||||
|
||||
@@ -6,4 +6,6 @@ export interface NotificationRepositoryInterface {
|
||||
save(notification: Notification): Promise<void>
|
||||
findByUserUuidUpdatedAfter(userUuid: Uuid, lastSyncTime: number): Promise<Notification[]>
|
||||
findByUserUuid(userUuid: Uuid): Promise<Notification[]>
|
||||
findByUserUuidAndType(userUuid: Uuid, type: NotificationType): Promise<Notification[]>
|
||||
remove(notification: Notification): Promise<void>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemHash } from '../Item/ItemHash'
|
||||
import { SharedVaultOperationOnItem } from './SharedVaultOperationOnItem'
|
||||
|
||||
describe('SharedVaultOperationOnItem', () => {
|
||||
let itemHash: ItemHash
|
||||
|
||||
beforeEach(() => {
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
})
|
||||
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = SharedVaultOperationOnItem.create({
|
||||
incomingItemHash: itemHash,
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
})
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return error if shared vault operation type is invalid', () => {
|
||||
const valueOrError = SharedVaultOperationOnItem.create({
|
||||
incomingItemHash: itemHash,
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
targetSharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: 'invalid',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
})
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if operation type is move to other shared vault and target shared vault uuid is not provided', () => {
|
||||
const valueOrError = SharedVaultOperationOnItem.create({
|
||||
incomingItemHash: itemHash,
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
targetSharedVaultUuid: undefined,
|
||||
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
})
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ValueObject, Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultOperationOnItemProps } from './SharedVaultOperationOnItemProps'
|
||||
|
||||
export class SharedVaultOperationOnItem extends ValueObject<SharedVaultOperationOnItemProps> {
|
||||
static readonly TYPES = {
|
||||
AddToSharedVault: 'add-to-shared-vault',
|
||||
RemoveFromSharedVault: 'remove-from-shared-vault',
|
||||
MoveToOtherSharedVault: 'move-to-other-shared-vault',
|
||||
SaveToSharedVault: 'save-to-shared-vault',
|
||||
CreateToSharedVault: 'create-to-shared-vault',
|
||||
}
|
||||
|
||||
private constructor(props: SharedVaultOperationOnItemProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(props: SharedVaultOperationOnItemProps): Result<SharedVaultOperationOnItem> {
|
||||
const isValidType = Object.values(this.TYPES).includes(props.type)
|
||||
if (!isValidType) {
|
||||
return Result.fail<SharedVaultOperationOnItem>(`Invalid shared vault operation type: ${props.type}`)
|
||||
}
|
||||
|
||||
if (props.type === this.TYPES.MoveToOtherSharedVault && !props.targetSharedVaultUuid) {
|
||||
return Result.fail<SharedVaultOperationOnItem>('Missing target shared vault uuid')
|
||||
}
|
||||
|
||||
return Result.ok<SharedVaultOperationOnItem>(new SharedVaultOperationOnItem(props))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from '../Item/Item'
|
||||
import { ItemHash } from '../Item/ItemHash'
|
||||
|
||||
export interface SharedVaultOperationOnItemProps {
|
||||
incomingItemHash: ItemHash
|
||||
userUuid: Uuid
|
||||
type: string
|
||||
sharedVaultUuid: Uuid
|
||||
targetSharedVaultUuid?: Uuid
|
||||
existingItem?: Item
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NotificationPayload, NotificationType, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
|
||||
import { RemoveNotificationsForUser } from './RemoveNotificationsForUser'
|
||||
import { Notification } from '../../../Notifications/Notification'
|
||||
|
||||
describe('RemoveNotificationsForUser', () => {
|
||||
let notificationRepository: NotificationRepositoryInterface
|
||||
let notification: Notification
|
||||
|
||||
const createUseCase = () => new RemoveNotificationsForUser(notificationRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
notification = Notification.create({
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
|
||||
payload: NotificationPayload.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
|
||||
version: '1.0',
|
||||
}).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
|
||||
notificationRepository = {} as jest.Mocked<NotificationRepositoryInterface>
|
||||
notificationRepository.findByUserUuidAndType = jest.fn().mockResolvedValue([notification])
|
||||
notificationRepository.remove = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove notifications for user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
type: NotificationType.TYPES.SharedVaultItemRemoved,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(notificationRepository.remove).toHaveBeenCalledWith(notification)
|
||||
})
|
||||
|
||||
it('should fail if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
type: NotificationType.TYPES.SharedVaultItemRemoved,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should fail if notification type is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
type: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NotificationType, Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { RemoveNotificationsForUserDTO } from './RemoveNotificationsForUserDTO'
|
||||
import { NotificationRepositoryInterface } from '../../../Notifications/NotificationRepositoryInterface'
|
||||
|
||||
export class RemoveNotificationsForUser implements UseCaseInterface<void> {
|
||||
constructor(private notificationRepository: NotificationRepositoryInterface) {}
|
||||
|
||||
async execute(dto: RemoveNotificationsForUserDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const typeOrError = NotificationType.create(dto.type)
|
||||
if (typeOrError.isFailed()) {
|
||||
return Result.fail(typeOrError.getError())
|
||||
}
|
||||
const type = typeOrError.getValue()
|
||||
|
||||
const notifications = await this.notificationRepository.findByUserUuidAndType(userUuid, type)
|
||||
for (const notification of notifications) {
|
||||
await this.notificationRepository.remove(notification)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface RemoveNotificationsForUserDTO {
|
||||
type: string
|
||||
userUuid: string
|
||||
}
|
||||
@@ -30,7 +30,7 @@ describe('DeclineInviteToSharedVault', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviteUuid: 'invalid',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
@@ -42,7 +42,7 @@ describe('DeclineInviteToSharedVault', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviteUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: 'invalid',
|
||||
userUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
@@ -56,7 +56,7 @@ describe('DeclineInviteToSharedVault', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviteUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
@@ -68,11 +68,11 @@ describe('DeclineInviteToSharedVault', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviteUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000001',
|
||||
userUuid: '00000000-0000-0000-0000-000000000001',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Only the recipient of the invite can decline it')
|
||||
expect(result.getError()).toBe('Only the recipient or the sender can decline the invite')
|
||||
})
|
||||
|
||||
it('should delete invite', async () => {
|
||||
@@ -80,7 +80,7 @@ describe('DeclineInviteToSharedVault', () => {
|
||||
|
||||
await useCase.execute({
|
||||
inviteUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(sharedVaultInviteRepository.remove).toHaveBeenCalled()
|
||||
|
||||
@@ -12,19 +12,19 @@ export class DeclineInviteToSharedVault implements UseCaseInterface<void> {
|
||||
}
|
||||
const inviteUuid = inviteUuidOrError.getValue()
|
||||
|
||||
const originatorUuidOrError = Uuid.create(dto.originatorUuid)
|
||||
if (originatorUuidOrError.isFailed()) {
|
||||
return Result.fail(originatorUuidOrError.getError())
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const originatorUuid = originatorUuidOrError.getValue()
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const invite = await this.sharedVaultInviteRepository.findByUuid(inviteUuid)
|
||||
if (!invite) {
|
||||
return Result.fail('Invite not found')
|
||||
}
|
||||
|
||||
if (!invite.props.userUuid.equals(originatorUuid)) {
|
||||
return Result.fail('Only the recipient of the invite can decline it')
|
||||
if (!invite.props.userUuid.equals(userUuid) && !invite.props.senderUuid.equals(userUuid)) {
|
||||
return Result.fail('Only the recipient or the sender can decline the invite')
|
||||
}
|
||||
|
||||
await this.sharedVaultInviteRepository.remove(invite)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface DeclineInviteToSharedVaultDTO {
|
||||
inviteUuid: string
|
||||
originatorUuid: string
|
||||
userUuid: string
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
originatorUuid: originatorUuid.value,
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
userUuid: sharedVaultUser.props.userUuid.value,
|
||||
forceRemoveOwner: true,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -29,7 +29,7 @@ export class DeleteSharedVaultInvitesSentByUser implements UseCaseInterface<void
|
||||
for (const invite of inboundInvites) {
|
||||
const result = await this.declineInviteToSharedVault.execute({
|
||||
inviteUuid: invite.id.toString(),
|
||||
originatorUuid: userUuid.value,
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
|
||||
@@ -20,7 +20,7 @@ export class DeleteSharedVaultInvitesToUser implements UseCaseInterface<void> {
|
||||
for (const invite of inboundInvites) {
|
||||
const result = await this.declineInviteToSharedVault.execute({
|
||||
inviteUuid: invite.id.toString(),
|
||||
originatorUuid: userUuid.value,
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { ContentType, Uuid, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { DetermineSharedVaultOperationOnItem } from './DetermineSharedVaultOperationOnItem'
|
||||
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
|
||||
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
|
||||
|
||||
describe('DetermineSharedVaultOperationOnItem', () => {
|
||||
let itemHash: ItemHash
|
||||
let existingItem: Item
|
||||
|
||||
const createUseCase = () => new DetermineSharedVaultOperationOnItem()
|
||||
|
||||
beforeEach(() => {
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
content: 'foobar',
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
|
||||
existingItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
})
|
||||
|
||||
it('should return an error if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
existingItem,
|
||||
itemHash,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
|
||||
})
|
||||
|
||||
it('should return an operation representing moving to another shared vault', async () => {
|
||||
existingItem = Item.create({
|
||||
...existingItem.props,
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: existingItem.uuid,
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
}).getValue()
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault)
|
||||
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000000')
|
||||
expect(result.getValue().props.targetSharedVaultUuid?.value).toEqual('00000000-0000-0000-0000-000000000001')
|
||||
})
|
||||
|
||||
it('should return an operation representing removing from shared vault', async () => {
|
||||
existingItem = Item.create({
|
||||
...existingItem.props,
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: existingItem.uuid,
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
}).getValue()
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault)
|
||||
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000000')
|
||||
})
|
||||
|
||||
it('should return an operation representing adding to shared vault', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.AddToSharedVault)
|
||||
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000001')
|
||||
})
|
||||
|
||||
it('should return an operation representing saving to shared vault', async () => {
|
||||
existingItem = Item.create({
|
||||
...existingItem.props,
|
||||
sharedVaultAssociation: SharedVaultAssociation.create({
|
||||
itemUuid: existingItem.uuid,
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
}).getValue()
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.SaveToSharedVault)
|
||||
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000000')
|
||||
})
|
||||
|
||||
it('should return an operation representing creating to shared vault', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem: null,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.CreateToSharedVault)
|
||||
expect(result.getValue().props.sharedVaultUuid.value).toEqual('00000000-0000-0000-0000-000000000001')
|
||||
})
|
||||
|
||||
it('should return an error if both existing and incoming item hash do not have shared vault uuid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem: null,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Invalid save operation')
|
||||
})
|
||||
|
||||
it('should return error if operation could not be determined based on input values', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem: null,
|
||||
itemHash,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Invalid save operation')
|
||||
})
|
||||
|
||||
it('should return error if shared vault operation on item could not be created', async () => {
|
||||
const mock = jest.spyOn(SharedVaultOperationOnItem, 'create')
|
||||
mock.mockImplementationOnce(() => Result.fail('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
existingItem: null,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
|
||||
}).getValue(),
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
|
||||
import { DetermineSharedVaultOperationOnItemDTO } from './DetermineSharedVaultOperationOnItemDTO'
|
||||
import { Item } from '../../../Item/Item'
|
||||
|
||||
export class DetermineSharedVaultOperationOnItem implements UseCaseInterface<SharedVaultOperationOnItem> {
|
||||
async execute(dto: DetermineSharedVaultOperationOnItemDTO): Promise<Result<SharedVaultOperationOnItem>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
let existingItemSharedVaultUuid = null
|
||||
if (dto.existingItem) {
|
||||
existingItemSharedVaultUuid = dto.existingItem.sharedVaultUuid
|
||||
}
|
||||
const targetItemSharedVaultUuid = dto.itemHash.sharedVaultUuid
|
||||
|
||||
if (!existingItemSharedVaultUuid && !targetItemSharedVaultUuid) {
|
||||
return Result.fail('Invalid save operation')
|
||||
}
|
||||
|
||||
const isMovingToOtherSharedVault =
|
||||
dto.existingItem &&
|
||||
existingItemSharedVaultUuid &&
|
||||
targetItemSharedVaultUuid &&
|
||||
!existingItemSharedVaultUuid.equals(targetItemSharedVaultUuid)
|
||||
const isRemovingFromSharedVault = dto.existingItem && existingItemSharedVaultUuid && !targetItemSharedVaultUuid
|
||||
const isAddingToSharedVault = dto.existingItem && !existingItemSharedVaultUuid && targetItemSharedVaultUuid
|
||||
const isSavingToSharedVault =
|
||||
dto.existingItem &&
|
||||
existingItemSharedVaultUuid &&
|
||||
targetItemSharedVaultUuid &&
|
||||
existingItemSharedVaultUuid.equals(targetItemSharedVaultUuid)
|
||||
|
||||
let operationOrError: Result<SharedVaultOperationOnItem>
|
||||
if (isMovingToOtherSharedVault) {
|
||||
operationOrError = SharedVaultOperationOnItem.create({
|
||||
existingItem: dto.existingItem as Item,
|
||||
sharedVaultUuid: existingItemSharedVaultUuid as Uuid,
|
||||
targetSharedVaultUuid: targetItemSharedVaultUuid,
|
||||
incomingItemHash: dto.itemHash,
|
||||
userUuid: userUuid,
|
||||
type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
|
||||
})
|
||||
} else if (isRemovingFromSharedVault) {
|
||||
operationOrError = SharedVaultOperationOnItem.create({
|
||||
existingItem: dto.existingItem as Item,
|
||||
sharedVaultUuid: existingItemSharedVaultUuid as Uuid,
|
||||
incomingItemHash: dto.itemHash,
|
||||
userUuid: userUuid,
|
||||
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
|
||||
})
|
||||
} else if (isAddingToSharedVault) {
|
||||
operationOrError = SharedVaultOperationOnItem.create({
|
||||
existingItem: dto.existingItem as Item,
|
||||
sharedVaultUuid: targetItemSharedVaultUuid,
|
||||
incomingItemHash: dto.itemHash,
|
||||
userUuid: userUuid,
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
})
|
||||
} else if (isSavingToSharedVault) {
|
||||
operationOrError = SharedVaultOperationOnItem.create({
|
||||
existingItem: dto.existingItem as Item,
|
||||
sharedVaultUuid: existingItemSharedVaultUuid as Uuid,
|
||||
incomingItemHash: dto.itemHash,
|
||||
userUuid: userUuid,
|
||||
type: SharedVaultOperationOnItem.TYPES.SaveToSharedVault,
|
||||
})
|
||||
} else {
|
||||
operationOrError = SharedVaultOperationOnItem.create({
|
||||
sharedVaultUuid: targetItemSharedVaultUuid as Uuid,
|
||||
incomingItemHash: dto.itemHash,
|
||||
userUuid: userUuid,
|
||||
type: SharedVaultOperationOnItem.TYPES.CreateToSharedVault,
|
||||
})
|
||||
}
|
||||
|
||||
if (operationOrError.isFailed()) {
|
||||
return Result.fail(operationOrError.getError())
|
||||
}
|
||||
|
||||
return Result.ok(operationOrError.getValue())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { Item } from '../../../Item/Item'
|
||||
|
||||
export interface DetermineSharedVaultOperationOnItemDTO {
|
||||
userUuid: string
|
||||
itemHash: ItemHash
|
||||
existingItem: Item | null
|
||||
}
|
||||
@@ -102,6 +102,26 @@ describe('RemoveUserFromSharedVault', () => {
|
||||
expect(result.getError()).toBe('Only owner can remove users from shared vault')
|
||||
})
|
||||
|
||||
it('should remove shared vault user if user is owner and is being force removed', async () => {
|
||||
sharedVault = SharedVault.create({
|
||||
fileUploadBytesLimit: 100,
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
|
||||
const useCase = createUseCase()
|
||||
await useCase.execute({
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000001',
|
||||
forceRemoveOwner: true,
|
||||
})
|
||||
|
||||
expect(sharedVaultUserRepository.remove).toHaveBeenCalledWith(sharedVaultUser)
|
||||
})
|
||||
|
||||
it('should return error when user is owner of shared vault', async () => {
|
||||
const useCase = createUseCase()
|
||||
const result = await useCase.execute({
|
||||
|
||||
@@ -42,7 +42,7 @@ export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
|
||||
}
|
||||
|
||||
const removingOwner = sharedVault.props.userUuid.equals(userUuid)
|
||||
if (removingOwner) {
|
||||
if (removingOwner && !dto.forceRemoveOwner) {
|
||||
return Result.fail('Owner cannot be removed from shared vault')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@ export interface RemoveUserFromSharedVaultDTO {
|
||||
sharedVaultUuid: string
|
||||
originatorUuid: string
|
||||
userUuid: string
|
||||
forceRemoveOwner?: boolean
|
||||
}
|
||||
|
||||
@@ -303,23 +303,6 @@ describe('SaveNewItem', () => {
|
||||
})
|
||||
|
||||
describe('when item hash represents a shared vault item', () => {
|
||||
it('returns a failure if the shared vault uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1 = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
shared_vault_uuid: '1-2-3',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000001',
|
||||
itemHash: itemHash1,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should create a shared vault association between the item and the shared vault', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
|
||||
@@ -87,17 +87,31 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
}
|
||||
const timestamps = timestampsOrError.getValue()
|
||||
|
||||
let sharedVaultAssociation = undefined
|
||||
if (dto.itemHash.representsASharedVaultItem()) {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.itemHash.props.shared_vault_uuid as string)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
const itemOrError = Item.create(
|
||||
{
|
||||
updatedWithSession,
|
||||
content: dto.itemHash.props.content ?? null,
|
||||
userUuid,
|
||||
contentType,
|
||||
encItemKey: dto.itemHash.props.enc_item_key ?? null,
|
||||
authHash: dto.itemHash.props.auth_hash ?? null,
|
||||
itemsKeyId: dto.itemHash.props.items_key_id ?? null,
|
||||
duplicateOf,
|
||||
deleted: dto.itemHash.props.deleted ?? false,
|
||||
dates,
|
||||
timestamps,
|
||||
},
|
||||
new UniqueEntityId(uuid.value),
|
||||
)
|
||||
if (itemOrError.isFailed()) {
|
||||
return Result.fail(itemOrError.getError())
|
||||
}
|
||||
const newItem = itemOrError.getValue()
|
||||
|
||||
if (dto.itemHash.representsASharedVaultItem()) {
|
||||
const sharedVaultAssociationOrError = SharedVaultAssociation.create({
|
||||
lastEditedBy: userUuid,
|
||||
sharedVaultUuid,
|
||||
sharedVaultUuid: dto.itemHash.sharedVaultUuid as Uuid,
|
||||
timestamps: Timestamps.create(
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
@@ -107,10 +121,9 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
if (sharedVaultAssociationOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultAssociationOrError.getError())
|
||||
}
|
||||
sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
|
||||
newItem.setSharedVaultAssociation(sharedVaultAssociationOrError.getValue())
|
||||
}
|
||||
|
||||
let keySystemAssociation = undefined
|
||||
if (dto.itemHash.hasDedicatedKeySystemAssociation()) {
|
||||
const keySystemIdentifiedValidationResult = Validator.isNotEmptyString(dto.itemHash.props.key_system_identifier)
|
||||
if (keySystemIdentifiedValidationResult.isFailed()) {
|
||||
@@ -129,32 +142,9 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
if (keySystemAssociationOrError.isFailed()) {
|
||||
return Result.fail(keySystemAssociationOrError.getError())
|
||||
}
|
||||
keySystemAssociation = keySystemAssociationOrError.getValue()
|
||||
newItem.setKeySystemAssociation(keySystemAssociationOrError.getValue())
|
||||
}
|
||||
|
||||
const itemOrError = Item.create(
|
||||
{
|
||||
updatedWithSession,
|
||||
content: dto.itemHash.props.content ?? null,
|
||||
userUuid,
|
||||
contentType,
|
||||
encItemKey: dto.itemHash.props.enc_item_key ?? null,
|
||||
authHash: dto.itemHash.props.auth_hash ?? null,
|
||||
itemsKeyId: dto.itemHash.props.items_key_id ?? null,
|
||||
duplicateOf,
|
||||
deleted: dto.itemHash.props.deleted ?? false,
|
||||
dates,
|
||||
timestamps,
|
||||
keySystemAssociation,
|
||||
sharedVaultAssociation,
|
||||
},
|
||||
new UniqueEntityId(uuid.value),
|
||||
)
|
||||
if (itemOrError.isFailed()) {
|
||||
return Result.fail(itemOrError.getError())
|
||||
}
|
||||
const newItem = itemOrError.getValue()
|
||||
|
||||
await this.itemRepository.save(newItem)
|
||||
|
||||
if (contentType.value !== null && [ContentType.TYPES.Note, ContentType.TYPES.File].includes(contentType.value)) {
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('SyncItems', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should sync items and return items keys on top for first sync', async () => {
|
||||
it('should sync items and return items keys on top for first sync that is not a shared vault exclusive sync', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemHashes: [itemHash],
|
||||
@@ -202,6 +202,32 @@ describe('SyncItems', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should sync items and not return items keys on top for first sync that is a shared vault exclusive sync', 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',
|
||||
sharedVaultUuids: ['00000000-0000-0000-0000-000000000000'],
|
||||
})
|
||||
expect(result.getValue()).toEqual({
|
||||
conflicts: [],
|
||||
cursorToken: 'asdzxc',
|
||||
retrievedItems: [item1],
|
||||
savedItems: [item2],
|
||||
syncToken: 'qwerty',
|
||||
sharedVaults: [],
|
||||
sharedVaultInvites: [],
|
||||
notifications: [],
|
||||
messages: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('should sync items and return filtered out sync conflicts for consecutive sync operations', async () => {
|
||||
getItemsUseCase.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
|
||||
@@ -50,7 +50,8 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
const saveItemsResult = saveItemsResultOrError.getValue()
|
||||
|
||||
let retrievedItems = this.filterOutSyncConflictsForConsecutiveSyncs(getItemsResult.items, saveItemsResult.conflicts)
|
||||
if (this.isFirstSync(dto)) {
|
||||
const isSharedVaultExclusiveSync = dto.sharedVaultUuids && dto.sharedVaultUuids.length > 0
|
||||
if (this.isFirstSync(dto) && !isSharedVaultExclusiveSync) {
|
||||
retrievedItems = await this.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,21 @@ import { Item } from '../../../Item/Item'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { UpdateExistingItem } from './UpdateExistingItem'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
|
||||
import {
|
||||
Uuid,
|
||||
ContentType,
|
||||
Dates,
|
||||
Timestamps,
|
||||
UniqueEntityId,
|
||||
Result,
|
||||
NotificationPayload,
|
||||
} from '@standardnotes/domain-core'
|
||||
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
|
||||
import { DetermineSharedVaultOperationOnItem } from '../../SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
|
||||
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
|
||||
import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
|
||||
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
|
||||
|
||||
describe('UpdateExistingItem', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
@@ -16,8 +28,21 @@ describe('UpdateExistingItem', () => {
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let itemHash1: ItemHash
|
||||
let item1: Item
|
||||
let determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem
|
||||
let addNotificationForUser: AddNotificationForUser
|
||||
let removeNotificationsForUser: RemoveNotificationsForUser
|
||||
|
||||
const createUseCase = () => new UpdateExistingItem(itemRepository, timer, domainEventPublisher, domainEventFactory, 5)
|
||||
const createUseCase = () =>
|
||||
new UpdateExistingItem(
|
||||
itemRepository,
|
||||
timer,
|
||||
domainEventPublisher,
|
||||
domainEventFactory,
|
||||
5,
|
||||
determineSharedVaultOperationOnItem,
|
||||
addNotificationForUser,
|
||||
removeNotificationsForUser,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
const timeHelper = new Timer()
|
||||
@@ -80,6 +105,25 @@ describe('UpdateExistingItem', () => {
|
||||
domainEventFactory.createItemRevisionCreationRequested = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
|
||||
determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockResolvedValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
existingItem: item1,
|
||||
incomingItemHash: itemHash1,
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
addNotificationForUser = {} as jest.Mocked<AddNotificationForUser>
|
||||
addNotificationForUser.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
removeNotificationsForUser = {} as jest.Mocked<RemoveNotificationsForUser>
|
||||
removeNotificationsForUser.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
})
|
||||
|
||||
it('should update item', async () => {
|
||||
@@ -306,12 +350,14 @@ describe('UpdateExistingItem', () => {
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
item1.props.sharedVaultAssociation = SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
item1.setSharedVaultAssociation(
|
||||
SharedVaultAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
)
|
||||
const idBefore = item1.props.sharedVaultAssociation?.id.toString()
|
||||
|
||||
const result = await useCase.execute({
|
||||
@@ -324,24 +370,7 @@ describe('UpdateExistingItem', () => {
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
|
||||
expect(item1.props.sharedVaultAssociation.id.toString()).toEqual(idBefore)
|
||||
})
|
||||
|
||||
it('should return error if shared vault uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const itemHash = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
shared_vault_uuid: 'invalid-uuid',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect((item1.props.sharedVaultAssociation as SharedVaultAssociation).id.toString()).toEqual(idBefore)
|
||||
})
|
||||
|
||||
it('should return error if shared vault association could not be created', async () => {
|
||||
@@ -366,6 +395,111 @@ describe('UpdateExistingItem', () => {
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('should return error if it fails to determine the shared vault operation on item', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const itemHash = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if it fails to add notification for user', async () => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
existingItem: item1,
|
||||
incomingItemHash: itemHash1,
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
addNotificationForUser.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const itemHash = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if it fails to create notification payload for user', async () => {
|
||||
determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
|
||||
Result.ok(
|
||||
SharedVaultOperationOnItem.create({
|
||||
existingItem: item1,
|
||||
incomingItemHash: itemHash1,
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const mock = jest.spyOn(NotificationPayload, 'create')
|
||||
mock.mockImplementation(() => {
|
||||
return Result.fail('Oops')
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const itemHash = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
mock.mockRestore()
|
||||
})
|
||||
|
||||
it('should return error if it fails to remove notifications for user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
removeNotificationsForUser.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const itemHash = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash,
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
performingUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when item is associated to a key system', () => {
|
||||
@@ -396,11 +530,13 @@ describe('UpdateExistingItem', () => {
|
||||
key_system_identifier: '00000000-0000-0000-0000-000000000000',
|
||||
}).getValue()
|
||||
|
||||
item1.props.keySystemAssociation = KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
item1.setKeySystemAssociation(
|
||||
KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
keySystemIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue(),
|
||||
)
|
||||
const idBefore = item1.props.keySystemAssociation?.id.toString()
|
||||
|
||||
const result = await useCase.execute({
|
||||
@@ -413,7 +549,7 @@ describe('UpdateExistingItem', () => {
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(item1.props.keySystemAssociation).not.toBeUndefined()
|
||||
expect(item1.props.keySystemAssociation.id.toString()).toEqual(idBefore)
|
||||
expect((item1.props.keySystemAssociation as KeySystemAssociation).id.toString()).toEqual(idBefore)
|
||||
})
|
||||
|
||||
it('should return error if key system identifier is invalid', async () => {
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { ContentType, Dates, Result, Timestamps, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core'
|
||||
import {
|
||||
ContentType,
|
||||
Dates,
|
||||
NotificationPayload,
|
||||
NotificationType,
|
||||
Result,
|
||||
Timestamps,
|
||||
UniqueEntityId,
|
||||
UseCaseInterface,
|
||||
Uuid,
|
||||
Validator,
|
||||
} from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
@@ -8,7 +19,10 @@ import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { DetermineSharedVaultOperationOnItem } from '../../SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
|
||||
import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
|
||||
import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
|
||||
import { RemoveNotificationsForUser } from '../../Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
|
||||
|
||||
export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
constructor(
|
||||
@@ -17,6 +31,9 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private revisionFrequency: number,
|
||||
private determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem,
|
||||
private addNotificationForUser: AddNotificationForUser,
|
||||
private removeNotificationsForUser: RemoveNotificationsForUser,
|
||||
) {}
|
||||
|
||||
async execute(dto: UpdateExistingItemDTO): Promise<Result<Item>> {
|
||||
@@ -105,55 +122,75 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
|
||||
dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
|
||||
|
||||
if (
|
||||
dto.itemHash.representsASharedVaultItem() &&
|
||||
!this.itemIsAlreadyAssociatedWithTheSharedVault(dto.existingItem, dto.itemHash)
|
||||
) {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.itemHash.props.shared_vault_uuid as string)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
let sharedVaultOperation: SharedVaultOperationOnItem | null = null
|
||||
if (dto.itemHash.representsASharedVaultItem()) {
|
||||
const sharedVaultAssociationOrError = SharedVaultAssociation.create(
|
||||
{
|
||||
lastEditedBy: userUuid,
|
||||
sharedVaultUuid: dto.itemHash.sharedVaultUuid as Uuid,
|
||||
timestamps: Timestamps.create(
|
||||
dto.existingItem.props.sharedVaultAssociation
|
||||
? dto.existingItem.props.sharedVaultAssociation.props.timestamps.createdAt
|
||||
: this.timer.getTimestampInMicroseconds(),
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue(),
|
||||
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
|
||||
},
|
||||
new UniqueEntityId(
|
||||
dto.existingItem.props.sharedVaultAssociation
|
||||
? dto.existingItem.props.sharedVaultAssociation.id.toString()
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
|
||||
const sharedVaultAssociationOrError = SharedVaultAssociation.create({
|
||||
lastEditedBy: userUuid,
|
||||
sharedVaultUuid,
|
||||
timestamps: Timestamps.create(
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue(),
|
||||
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
|
||||
})
|
||||
if (sharedVaultAssociationOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultAssociationOrError.getError())
|
||||
}
|
||||
|
||||
dto.existingItem.props.sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
|
||||
dto.existingItem.setSharedVaultAssociation(sharedVaultAssociationOrError.getValue())
|
||||
|
||||
const sharedVaultOperationOrError = await this.determineSharedVaultOperationOnItem.execute({
|
||||
existingItem: dto.existingItem,
|
||||
itemHash: dto.itemHash,
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
if (sharedVaultOperationOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultOperationOrError.getError())
|
||||
}
|
||||
sharedVaultOperation = sharedVaultOperationOrError.getValue()
|
||||
} else {
|
||||
dto.existingItem.unsetSharedVaultAssociation()
|
||||
}
|
||||
|
||||
if (
|
||||
dto.itemHash.hasDedicatedKeySystemAssociation() &&
|
||||
!this.itemIsAlreadyAssociatedWithTheKeySystem(dto.existingItem, dto.itemHash)
|
||||
) {
|
||||
if (dto.itemHash.hasDedicatedKeySystemAssociation()) {
|
||||
const keySystemIdentifiedValidationResult = Validator.isNotEmptyString(dto.itemHash.props.key_system_identifier)
|
||||
if (keySystemIdentifiedValidationResult.isFailed()) {
|
||||
return Result.fail(keySystemIdentifiedValidationResult.getError())
|
||||
}
|
||||
const keySystemIdentifier = dto.itemHash.props.key_system_identifier as string
|
||||
|
||||
const keySystemAssociationOrError = KeySystemAssociation.create({
|
||||
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
|
||||
timestamps: Timestamps.create(
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue(),
|
||||
keySystemIdentifier,
|
||||
})
|
||||
const keySystemAssociationOrError = KeySystemAssociation.create(
|
||||
{
|
||||
itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
|
||||
timestamps: Timestamps.create(
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue(),
|
||||
keySystemIdentifier,
|
||||
},
|
||||
new UniqueEntityId(
|
||||
dto.existingItem.props.keySystemAssociation
|
||||
? dto.existingItem.props.keySystemAssociation.id.toString()
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
if (keySystemAssociationOrError.isFailed()) {
|
||||
return Result.fail(keySystemAssociationOrError.getError())
|
||||
}
|
||||
|
||||
dto.existingItem.props.keySystemAssociation = keySystemAssociationOrError.getValue()
|
||||
dto.existingItem.setKeySystemAssociation(keySystemAssociationOrError.getValue())
|
||||
} else {
|
||||
dto.existingItem.unsetKeySystemAssociation()
|
||||
}
|
||||
|
||||
if (dto.itemHash.props.deleted === true) {
|
||||
@@ -190,20 +227,55 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
)
|
||||
}
|
||||
|
||||
const notificationsResult = await this.addNotifications(dto.existingItem.uuid, userUuid, sharedVaultOperation)
|
||||
if (notificationsResult.isFailed()) {
|
||||
return Result.fail(notificationsResult.getError())
|
||||
}
|
||||
|
||||
return Result.ok(dto.existingItem)
|
||||
}
|
||||
|
||||
private itemIsAlreadyAssociatedWithTheSharedVault(item: Item, itemHash: ItemHash): boolean {
|
||||
return (
|
||||
item.props.sharedVaultAssociation !== undefined &&
|
||||
item.props.sharedVaultAssociation.props.sharedVaultUuid.value === itemHash.props.shared_vault_uuid
|
||||
)
|
||||
}
|
||||
private async addNotifications(
|
||||
itemUuid: Uuid,
|
||||
userUuid: Uuid,
|
||||
sharedVaultOperation: SharedVaultOperationOnItem | null,
|
||||
): Promise<Result<void>> {
|
||||
if (
|
||||
sharedVaultOperation &&
|
||||
sharedVaultOperation.props.type === SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault
|
||||
) {
|
||||
const notificationPayloadOrError = NotificationPayload.create({
|
||||
sharedVaultUuid: sharedVaultOperation.props.sharedVaultUuid,
|
||||
type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
|
||||
itemUuid: itemUuid,
|
||||
version: '1.0',
|
||||
})
|
||||
if (notificationPayloadOrError.isFailed()) {
|
||||
return Result.fail(notificationPayloadOrError.getError())
|
||||
}
|
||||
const payload = notificationPayloadOrError.getValue()
|
||||
|
||||
private itemIsAlreadyAssociatedWithTheKeySystem(item: Item, itemHash: ItemHash): boolean {
|
||||
return (
|
||||
item.props.keySystemAssociation !== undefined &&
|
||||
item.props.keySystemAssociation.props.keySystemIdentifier === itemHash.props.key_system_identifier
|
||||
)
|
||||
const result = await this.addNotificationForUser.execute({
|
||||
payload,
|
||||
type: NotificationType.TYPES.SharedVaultItemRemoved,
|
||||
userUuid: userUuid.value,
|
||||
version: '1.0',
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedVaultOperation && sharedVaultOperation.props.type === SharedVaultOperationOnItem.TYPES.AddToSharedVault) {
|
||||
const result = await this.removeNotificationsForUser.execute({
|
||||
type: NotificationType.TYPES.SharedVaultItemRemoved,
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ export class HomeServerSharedVaultInvitesController extends BaseHttpController {
|
||||
async declineSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.declineSharedVaultInviteUseCase.execute({
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -239,7 +239,7 @@ export class HomeServerSharedVaultInvitesController extends BaseHttpController {
|
||||
async deleteSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.declineSharedVaultInviteUseCase.execute({
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReadStream } from 'fs'
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { Change, MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemQuery } from '../../Domain/Item/ItemQuery'
|
||||
@@ -10,6 +10,8 @@ import { TypeORMItem } from './TypeORMItem'
|
||||
import { KeySystemAssociationRepositoryInterface } from '../../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
|
||||
import { SharedVaultAssociationRepositoryInterface } from '../../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
|
||||
import { TypeORMSharedVaultAssociation } from './TypeORMSharedVaultAssociation'
|
||||
import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation'
|
||||
import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation'
|
||||
|
||||
export class TypeORMItemRepository implements ItemRepositoryInterface {
|
||||
constructor(
|
||||
@@ -24,13 +26,7 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
|
||||
|
||||
await this.ormRepository.save(persistence)
|
||||
|
||||
if (item.props.sharedVaultAssociation) {
|
||||
await this.sharedVaultAssociationRepository.save(item.props.sharedVaultAssociation)
|
||||
}
|
||||
|
||||
if (item.props.keySystemAssociation) {
|
||||
await this.keySystemAssociationRepository.save(item.props.keySystemAssociation)
|
||||
}
|
||||
await this.persistAssociationChanges(item)
|
||||
}
|
||||
|
||||
async remove(item: Item): Promise<void> {
|
||||
@@ -273,4 +269,27 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
|
||||
item.props.sharedVaultAssociation = sharedVaultAssociation
|
||||
}
|
||||
}
|
||||
|
||||
private async persistAssociationChanges(item: Item): Promise<void> {
|
||||
for (const change of item.getChanges()) {
|
||||
if (change.props.changeData instanceof SharedVaultAssociation) {
|
||||
if ([Change.TYPES.Add, Change.TYPES.Modify].includes(change.props.changeType)) {
|
||||
await this.sharedVaultAssociationRepository.save(change.props.changeData)
|
||||
}
|
||||
if (change.props.changeType === Change.TYPES.Remove) {
|
||||
await this.sharedVaultAssociationRepository.remove(change.props.changeData)
|
||||
}
|
||||
}
|
||||
if (change.props.changeData instanceof KeySystemAssociation) {
|
||||
if ([Change.TYPES.Add, Change.TYPES.Modify].includes(change.props.changeType)) {
|
||||
await this.keySystemAssociationRepository.save(change.props.changeData)
|
||||
}
|
||||
if (change.props.changeType === Change.TYPES.Remove) {
|
||||
await this.keySystemAssociationRepository.remove(change.props.changeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item.flushChanges()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Repository } from 'typeorm'
|
||||
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { MapperInterface, NotificationType, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { NotificationRepositoryInterface } from '../../Domain/Notifications/NotificationRepositoryInterface'
|
||||
import { TypeORMNotification } from './TypeORMNotification'
|
||||
@@ -11,6 +11,24 @@ export class TypeORMNotificationRepository implements NotificationRepositoryInte
|
||||
private mapper: MapperInterface<Notification, TypeORMNotification>,
|
||||
) {}
|
||||
|
||||
async findByUserUuidAndType(userUuid: Uuid, type: NotificationType): Promise<Notification[]> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('notification')
|
||||
.where('notification.user_uuid = :userUuid', {
|
||||
userUuid: userUuid.value,
|
||||
})
|
||||
.andWhere('notification.type = :type', {
|
||||
type: type.value,
|
||||
})
|
||||
.getMany()
|
||||
|
||||
return persistence.map((p) => this.mapper.toDomain(p))
|
||||
}
|
||||
|
||||
async remove(notification: Notification): Promise<void> {
|
||||
await this.ormRepository.remove(this.mapper.toProjection(notification))
|
||||
}
|
||||
|
||||
async save(sharedVault: Notification): Promise<void> {
|
||||
const persistence = this.mapper.toProjection(sharedVault)
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.6](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.5...@standardnotes/websockets-server@1.10.6) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.10.5](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.4...@standardnotes/websockets-server@1.10.5) (2023-07-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.10.5",
|
||||
"version": "1.10.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user