mirror of
https://github.com/standardnotes/server
synced 2026-05-14 21:57:35 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fae4553fc8 | |||
| cb74b23e45 | |||
| af8f12c33a |
@@ -127,4 +127,4 @@ jobs:
|
||||
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
|
||||
|
||||
- name: Run E2E Test Suite
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html?skip_paid_features=true
|
||||
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
|
||||
|
||||
@@ -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.
|
||||
|
||||
# [2.25.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.9...@standardnotes/analytics@2.25.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [2.24.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.24.8...@standardnotes/analytics@2.24.9) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.24.9",
|
||||
"version": "2.25.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Result, Entity, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { StatisticMeasureProps } from './StatisticMeasureProps'
|
||||
|
||||
export class StatisticMeasure extends Entity<StatisticMeasureProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.props.name.value
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { SubscriptionProps } from './SubscriptionProps'
|
||||
|
||||
export class Subscription extends Entity<SubscriptionProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: SubscriptionProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { UserProps } from './UserProps'
|
||||
|
||||
export class User extends Entity<UserProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: UserProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -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.65.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.65.6...@standardnotes/api-gateway@1.65.7) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.65.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.65.5...@standardnotes/api-gateway@1.65.6) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.65.6",
|
||||
"version": "1.65.7",
|
||||
"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.125.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.124.2...@standardnotes/auth-server@1.125.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [1.124.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.124.1...@standardnotes/auth-server@1.124.2) (2023-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.124.2",
|
||||
"version": "1.125.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { AuthenticatorProps } from './AuthenticatorProps'
|
||||
|
||||
export class Authenticator extends Entity<AuthenticatorProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: AuthenticatorProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { AuthenticatorChallengeProps } from './AuthenticatorChallengeProps'
|
||||
|
||||
export class AuthenticatorChallenge extends Entity<AuthenticatorChallengeProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: AuthenticatorChallengeProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { EmergencyAccessInvitationProps } from './EmergencyAccessInvitationProps'
|
||||
|
||||
export class EmergencyAccessInvitation extends Entity<EmergencyAccessInvitationProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: EmergencyAccessInvitationProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { SessionTraceProps } from './SessionTraceProps'
|
||||
|
||||
export class SessionTrace extends Entity<SessionTraceProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: SessionTraceProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -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.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.22.0...@standardnotes/domain-core@1.23.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.21.1...@standardnotes/domain-core@1.22.0) (2023-07-12)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.22.0",
|
||||
"version": "1.23.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -4,10 +4,6 @@ import { UniqueEntityId } from '../Core/UniqueEntityId'
|
||||
import { CacheEntryProps } from './CacheEntryProps'
|
||||
|
||||
export class CacheEntry extends Entity<CacheEntryProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: CacheEntryProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { Entity } from './Entity'
|
||||
import { UniqueEntityId } from './UniqueEntityId'
|
||||
|
||||
export abstract class Aggregate<T> extends Entity<T> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
}
|
||||
export abstract class Aggregate<T> extends Entity<T> {}
|
||||
|
||||
@@ -9,6 +9,10 @@ export abstract class Entity<T> {
|
||||
this._id = id ? id : new UniqueEntityId()
|
||||
}
|
||||
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
public equals(object?: Entity<T>): boolean {
|
||||
if (object == null || object == undefined) {
|
||||
return false
|
||||
|
||||
@@ -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.7](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.6...@standardnotes/event-store@1.11.7) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.6](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.5...@standardnotes/event-store@1.11.6) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.6",
|
||||
"version": "1.11.7",
|
||||
"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.9](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.8...@standardnotes/files-server@1.19.9) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.19.8](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.7...@standardnotes/files-server@1.19.8) (2023-07-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.19.8",
|
||||
"version": "1.19.9",
|
||||
"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.12.4](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.12.3...@standardnotes/home-server@1.12.4) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.12.3](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.12.2...@standardnotes/home-server@1.12.3) (2023-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.12.3",
|
||||
"version": "1.12.4",
|
||||
"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.25.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.24.1...@standardnotes/revisions-server@1.25.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/server/issues/659)) ([cb74b23](https://github.com/standardnotes/server/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [1.24.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.24.0...@standardnotes/revisions-server@1.24.1) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.24.1",
|
||||
"version": "1.25.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { RevisionProps } from './RevisionProps'
|
||||
|
||||
export class Revision extends Entity<RevisionProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: RevisionProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { RevisionMetadataProps } from './RevisionMetadataProps'
|
||||
|
||||
export class RevisionMetadata extends Entity<RevisionMetadataProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: RevisionMetadataProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -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.9](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.8...@standardnotes/scheduler-server@1.20.9) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.8](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.7...@standardnotes/scheduler-server@1.20.8) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.8",
|
||||
"version": "1.20.9",
|
||||
"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.14](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.13...@standardnotes/settings@1.21.14) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.13](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.12...@standardnotes/settings@1.21.13) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/settings",
|
||||
"version": "1.21.13",
|
||||
"version": "1.21.14",
|
||||
"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.64.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.63.1...@standardnotes/syncing-server@1.64.0) (2023-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** refactor syncing to decouple getting and saving items ([#659](https://github.com/standardnotes/syncing-server-js/issues/659)) ([cb74b23](https://github.com/standardnotes/syncing-server-js/commit/cb74b23e45b207136e299ce8a3db2c04dc87e21e))
|
||||
|
||||
## [1.63.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.63.0...@standardnotes/syncing-server@1.63.1) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.63.1",
|
||||
"version": "1.64.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -23,8 +23,6 @@ import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { ItemTransferCalculatorInterface } from '../Domain/Item/ItemTransferCalculatorInterface'
|
||||
import { ItemTransferCalculator } from '../Domain/Item/ItemTransferCalculator'
|
||||
import { ItemConflict } from '../Domain/Item/ItemConflict'
|
||||
import { ItemService } from '../Domain/Item/ItemService'
|
||||
import { ItemServiceInterface } from '../Domain/Item/ItemServiceInterface'
|
||||
import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter'
|
||||
import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter'
|
||||
import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter'
|
||||
@@ -75,6 +73,11 @@ import { ItemBackupRepresentation } from '../Mapping/Backup/ItemBackupRepresenta
|
||||
import { ItemBackupMapper } from '../Mapping/Backup/ItemBackupMapper'
|
||||
import { SaveNewItem } from '../Domain/UseCase/Syncing/SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
|
||||
import { GetItems } from '../Domain/UseCase/Syncing/GetItems/GetItems'
|
||||
import { SaveItems } from '../Domain/UseCase/Syncing/SaveItems/SaveItems'
|
||||
import { ItemHashHttpMapper } from '../Mapping/Http/ItemHashHttpMapper'
|
||||
import { ItemHash } from '../Domain/Item/ItemHash'
|
||||
import { ItemHashHttpRepresentation } from '../Mapping/Http/ItemHashHttpRepresentation'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -209,6 +212,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
|
||||
.toConstantValue(new ItemPersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<ItemHash, ItemHashHttpRepresentation>>(TYPES.Sync_ItemHashHttpMapper)
|
||||
.toConstantValue(new ItemHashHttpMapper())
|
||||
container
|
||||
.bind<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper)
|
||||
.toConstantValue(new ItemHttpMapper(container.get(TYPES.Sync_Timer)))
|
||||
@@ -217,7 +223,12 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(new SavedItemHttpMapper(container.get(TYPES.Sync_Timer)))
|
||||
container
|
||||
.bind<MapperInterface<ItemConflict, ItemConflictHttpRepresentation>>(TYPES.Sync_ItemConflictHttpMapper)
|
||||
.toConstantValue(new ItemConflictHttpMapper(container.get(TYPES.Sync_ItemHttpMapper)))
|
||||
.toConstantValue(
|
||||
new ItemConflictHttpMapper(
|
||||
container.get(TYPES.Sync_ItemHttpMapper),
|
||||
container.get(TYPES.Sync_ItemHashHttpMapper),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
|
||||
.toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
|
||||
@@ -282,16 +293,35 @@ export class ContainerConfigLoader {
|
||||
env.get('MAX_ITEMS_LIMIT', true) ? +env.get('MAX_ITEMS_LIMIT', true) : this.DEFAULT_MAX_ITEMS_LIMIT,
|
||||
)
|
||||
|
||||
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<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
|
||||
.toConstantValue(
|
||||
new ItemSaveValidator([
|
||||
container.get(TYPES.Sync_OwnershipFilter),
|
||||
container.get(TYPES.Sync_TimeDifferenceFilter),
|
||||
container.get(TYPES.Sync_ContentTypeFilter),
|
||||
container.get(TYPES.Sync_ContentFilter),
|
||||
]),
|
||||
)
|
||||
|
||||
// use cases
|
||||
container.bind<SyncItems>(TYPES.Sync_SyncItems).toDynamicValue((context: interfaces.Context) => {
|
||||
return new SyncItems(context.container.get(TYPES.Sync_ItemService))
|
||||
})
|
||||
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
container
|
||||
.bind<GetItems>(TYPES.Sync_GetItems)
|
||||
.toConstantValue(
|
||||
new GetItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SaveNewItem>(TYPES.Sync_SaveNewItem)
|
||||
.toConstantValue(
|
||||
@@ -313,41 +343,35 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Sync_REVISIONS_FREQUENCY),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
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<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new ItemSaveValidator([
|
||||
context.container.get(TYPES.Sync_OwnershipFilter),
|
||||
context.container.get(TYPES.Sync_TimeDifferenceFilter),
|
||||
context.container.get(TYPES.Sync_ContentTypeFilter),
|
||||
context.container.get(TYPES.Sync_ContentFilter),
|
||||
])
|
||||
})
|
||||
|
||||
container
|
||||
.bind<ItemServiceInterface>(TYPES.Sync_ItemService)
|
||||
.bind<SaveItems>(TYPES.Sync_SaveItems)
|
||||
.toConstantValue(
|
||||
new ItemService(
|
||||
new SaveItems(
|
||||
container.get(TYPES.Sync_ItemSaveValidator),
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT),
|
||||
container.get(TYPES.Sync_ItemTransferCalculator),
|
||||
container.get(TYPES.Sync_Timer),
|
||||
container.get(TYPES.Sync_MAX_ITEMS_LIMIT),
|
||||
container.get(TYPES.Sync_SaveNewItem),
|
||||
container.get(TYPES.Sync_UpdateExistingItem),
|
||||
container.get(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SyncItems>(TYPES.Sync_SyncItems)
|
||||
.toConstantValue(
|
||||
new SyncItems(
|
||||
container.get(TYPES.Sync_ItemRepository),
|
||||
container.get(TYPES.Sync_GetItems),
|
||||
container.get(TYPES.Sync_SaveItems),
|
||||
),
|
||||
)
|
||||
container.bind<CheckIntegrity>(TYPES.Sync_CheckIntegrity).toDynamicValue((context: interfaces.Context) => {
|
||||
return new CheckIntegrity(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
container.bind<GetItem>(TYPES.Sync_GetItem).toDynamicValue((context: interfaces.Context) => {
|
||||
return new GetItem(context.container.get(TYPES.Sync_ItemRepository))
|
||||
})
|
||||
|
||||
// Services
|
||||
container
|
||||
.bind<SyncResponseFactory20161215>(TYPES.Sync_SyncResponseFactory20161215)
|
||||
.toConstantValue(new SyncResponseFactory20161215(container.get(TYPES.Sync_ItemHttpMapper)))
|
||||
|
||||
@@ -55,6 +55,8 @@ const TYPES = {
|
||||
Sync_DeleteMessage: Symbol.for('Sync_DeleteMessage'),
|
||||
Sync_SaveNewItem: Symbol.for('Sync_SaveNewItem'),
|
||||
Sync_UpdateExistingItem: Symbol.for('Sync_UpdateExistingItem'),
|
||||
Sync_GetItems: Symbol.for('Sync_GetItems'),
|
||||
Sync_SaveItems: Symbol.for('Sync_SaveItems'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
@@ -67,7 +69,6 @@ const TYPES = {
|
||||
Sync_DomainEventFactory: Symbol.for('Sync_DomainEventFactory'),
|
||||
Sync_DomainEventMessageHandler: Symbol.for('Sync_DomainEventMessageHandler'),
|
||||
Sync_HTTPClient: Symbol.for('Sync_HTTPClient'),
|
||||
Sync_ItemService: Symbol.for('Sync_ItemService'),
|
||||
Sync_Timer: Symbol.for('Sync_Timer'),
|
||||
Sync_SyncResponseFactory20161215: Symbol.for('Sync_SyncResponseFactory20161215'),
|
||||
Sync_SyncResponseFactory20200115: Symbol.for('Sync_SyncResponseFactory20200115'),
|
||||
@@ -90,6 +91,7 @@ const TYPES = {
|
||||
Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
|
||||
Sync_ItemPersistenceMapper: Symbol.for('Sync_ItemPersistenceMapper'),
|
||||
Sync_ItemHttpMapper: Symbol.for('Sync_ItemHttpMapper'),
|
||||
Sync_ItemHashHttpMapper: Symbol.for('Sync_ItemHashHttpMapper'),
|
||||
Sync_SavedItemHttpMapper: Symbol.for('Sync_SavedItemHttpMapper'),
|
||||
Sync_ItemConflictHttpMapper: Symbol.for('Sync_ItemConflictHttpMapper'),
|
||||
Sync_ItemBackupMapper: Symbol.for('Sync_ItemBackupMapper'),
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Item } from './Item'
|
||||
|
||||
export type GetItemsResult = {
|
||||
items: Array<Item>
|
||||
cursorToken?: string
|
||||
}
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { ItemProps } from './ItemProps'
|
||||
|
||||
export class Item extends Entity<ItemProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: ItemProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
export type ItemHash = {
|
||||
uuid: string
|
||||
content?: string
|
||||
content_type: string | null
|
||||
deleted?: boolean
|
||||
duplicate_of?: string | null
|
||||
auth_hash?: string
|
||||
enc_item_key?: string
|
||||
items_key_id?: string
|
||||
created_at?: string
|
||||
created_at_timestamp?: number
|
||||
updated_at?: string
|
||||
updated_at_timestamp?: number
|
||||
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemHashProps } from './ItemHashProps'
|
||||
|
||||
export class ItemHash extends ValueObject<ItemHashProps> {
|
||||
private constructor(props: ItemHashProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(props: ItemHashProps): Result<ItemHash> {
|
||||
return Result.ok<ItemHash>(new ItemHash(props))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface ItemHashProps {
|
||||
uuid: string
|
||||
user_uuid: string
|
||||
content?: string
|
||||
content_type: string | null
|
||||
deleted?: boolean
|
||||
duplicate_of?: string | null
|
||||
auth_hash?: string
|
||||
enc_item_key?: string
|
||||
items_key_id?: string
|
||||
key_system_identifier: string | null
|
||||
shared_vault_uuid: string | null
|
||||
created_at?: string
|
||||
created_at_timestamp?: number
|
||||
updated_at?: string
|
||||
updated_at_timestamp?: number
|
||||
}
|
||||
@@ -1,785 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { Item } from './Item'
|
||||
import { ItemHash } from './ItemHash'
|
||||
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
import { ItemService } from './ItemService'
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
|
||||
import { ItemConflict } from './ItemConflict'
|
||||
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
|
||||
import { SaveNewItem } from '../UseCase/Syncing/SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('ItemService', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
const contentSizeTransferLimit = 100
|
||||
let timer: TimerInterface
|
||||
let item1: Item
|
||||
let item2: Item
|
||||
let itemHash1: ItemHash
|
||||
let itemHash2: ItemHash
|
||||
let syncToken: string
|
||||
let logger: Logger
|
||||
let itemSaveValidator: ItemSaveValidatorInterface
|
||||
let newItem: Item
|
||||
let timeHelper: Timer
|
||||
let itemTransferCalculator: ItemTransferCalculatorInterface
|
||||
let saveNewItemUseCase: SaveNewItem
|
||||
let updateExistingItemUseCase: UpdateExistingItem
|
||||
const maxItemsSyncLimit = 300
|
||||
|
||||
const createService = () =>
|
||||
new ItemService(
|
||||
itemSaveValidator,
|
||||
itemRepository,
|
||||
contentSizeTransferLimit,
|
||||
itemTransferCalculator,
|
||||
timer,
|
||||
maxItemsSyncLimit,
|
||||
saveNewItemUseCase,
|
||||
updateExistingItemUseCase,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
timeHelper = new Timer()
|
||||
|
||||
item1 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
item2 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue()
|
||||
|
||||
itemHash1 = {
|
||||
uuid: '1-2-3',
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe1',
|
||||
items_key_id: 'asdasd1',
|
||||
created_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
itemHash2 = {
|
||||
uuid: '2-3-4',
|
||||
content: 'asdqwe2',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe2',
|
||||
items_key_id: 'asdasd2',
|
||||
created_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(item2.props.timestamps.createdAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item2.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest
|
||||
.fn()
|
||||
.mockReturnValue([item1.id.toString(), item2.id.toString()])
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item1, item2])
|
||||
itemRepository.countAll = jest.fn().mockReturnValue(2)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1616164633241568)
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date())
|
||||
timer.convertStringDateToDate = jest
|
||||
.fn()
|
||||
.mockImplementation((date: string) => timeHelper.convertStringDateToDate(date))
|
||||
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(600)
|
||||
timer.convertStringDateToMicroseconds = jest
|
||||
.fn()
|
||||
.mockImplementation((date: string) => timeHelper.convertStringDateToMicroseconds(date))
|
||||
timer.convertMicrosecondsToDate = jest
|
||||
.fn()
|
||||
.mockImplementation((microseconds: number) => timeHelper.convertMicrosecondsToDate(microseconds))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
|
||||
syncToken = Buffer.from('2:1616164633.241564', 'utf-8').toString('base64')
|
||||
|
||||
itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
|
||||
itemSaveValidator.validate = jest.fn().mockReturnValue({ passed: true })
|
||||
|
||||
newItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241313), new Date(1616164633241313)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241313, 1616164633241313).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
|
||||
).getValue()
|
||||
|
||||
saveNewItemUseCase = {} as jest.Mocked<SaveNewItem>
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(newItem))
|
||||
|
||||
updateExistingItemUseCase = {} as jest.Mocked<UpdateExistingItem>
|
||||
updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(item1))
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from last sync with sync token version 1', async () => {
|
||||
syncToken = Buffer.from('1:2021-03-15 07:00:00', 'utf-8').toString('base64')
|
||||
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1615791600000000,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortOrder: 'ASC',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from last sync', async () => {
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from last sync with upper bound items limit', async () => {
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
limit: 1000,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 300,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve no items for a user if there are none from last sync', async () => {
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockReturnValue([])
|
||||
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [],
|
||||
})
|
||||
|
||||
expect(itemRepository.findAll).not.toHaveBeenCalled()
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a cursor token if there are more items than requested with limit', async () => {
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item1])
|
||||
|
||||
const itemsResponse = await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
limit: 1,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
|
||||
expect(itemsResponse).toEqual({
|
||||
cursorToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMQ==',
|
||||
items: [item1],
|
||||
})
|
||||
|
||||
expect(Buffer.from(<string>itemsResponse.cursorToken, 'base64').toString('utf-8')).toEqual('2:1616164633.241311')
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 1,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items for a user from cursor token', async () => {
|
||||
const cursorToken = Buffer.from('2:1616164633.241123', 'utf-8').toString('base64')
|
||||
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
cursorToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241123,
|
||||
syncTimeComparison: '>=',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all undeleted items for a user without cursor or sync token', async () => {
|
||||
expect(
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
contentType: ContentType.TYPES.Note,
|
||||
}),
|
||||
).toEqual({
|
||||
items: [item1, item2],
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
deleted: false,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
syncTimeComparison: '>',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items with default limit if not defined', async () => {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortOrder: 'ASC',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all items with non-positive limit if not defined', async () => {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken,
|
||||
limit: 0,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
|
||||
expect(itemRepository.countAll).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
lastSyncTime: 1616164633241564,
|
||||
syncTimeComparison: '>',
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
userUuid: '1-2-3',
|
||||
limit: 150,
|
||||
})
|
||||
expect(itemRepository.findAll).toHaveBeenCalledWith({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the sync token is missing time', async () => {
|
||||
let error = null
|
||||
|
||||
try {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken: '2:',
|
||||
limit: 0,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should throw an error if the sync token is missing version', async () => {
|
||||
let error = null
|
||||
|
||||
try {
|
||||
await createService().getItems({
|
||||
userUuid: '1-2-3',
|
||||
syncToken: '1234567890',
|
||||
limit: 0,
|
||||
contentType: ContentType.TYPES.Note,
|
||||
})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should front load keys items to top of the collection for better client performance', async () => {
|
||||
const item3 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue()
|
||||
const item4 = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar2',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241312), new Date(1616164633241312)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241312, 1616164633241312).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000004'),
|
||||
).getValue()
|
||||
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item3, item4])
|
||||
|
||||
await createService().frontLoadKeysItemsToTop('1-2-3', [item1, item2])
|
||||
})
|
||||
|
||||
it('should save new items', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [newItem],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxNA==',
|
||||
})
|
||||
|
||||
expect(saveNewItemUseCase.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not save new items in read only access mode', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: true,
|
||||
sessionUuid: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'readonly_error',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
|
||||
expect(saveNewItemUseCase.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save new items that are duplicates', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
const duplicateItem = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar1',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241570), new Date(1616164633241570)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241570, 1616164633241570).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000005'),
|
||||
).getValue()
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.ok(duplicateItem))
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [duplicateItem],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU3MQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip items that are conflicting on validation', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const conflict = {} as jest.Mocked<ItemConflict>
|
||||
const validationResult = { passed: false, conflict }
|
||||
itemSaveValidator.validate = jest.fn().mockReturnValue(validationResult)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [conflict],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark items as saved that are skipped on validation', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const skipped = item1
|
||||
const validationResult = { passed: false, skipped }
|
||||
itemSaveValidator.validate = jest.fn().mockReturnValue(validationResult)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [skipped],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMg==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should calculate the sync token based on last updated date of saved items incremented with 1 microsecond to avoid returning same object in subsequent sync', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const itemHash3 = {
|
||||
uuid: '3-4-5',
|
||||
content: 'asdqwe3',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'qweqwe3',
|
||||
items_key_id: 'asdasd3',
|
||||
created_at: '2021-02-19T11:35:45.652Z',
|
||||
updated_at: '2021-03-25T09:37:37.943Z',
|
||||
} as jest.Mocked<ItemHash>
|
||||
|
||||
const saveProcedureStartTimestamp = 1616164633241580
|
||||
const item1Timestamp = 1616164633241570
|
||||
const item2Timestamp = 1616164633241568
|
||||
const item3Timestamp = 1616164633241569
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValueOnce(saveProcedureStartTimestamp)
|
||||
|
||||
saveNewItemUseCase.execute = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item1.props,
|
||||
timestamps: Timestamps.create(item1Timestamp, item1Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item2.props,
|
||||
timestamps: Timestamps.create(item2Timestamp, item2Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000002'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
Result.ok(
|
||||
Item.create(
|
||||
{
|
||||
...item2.props,
|
||||
timestamps: Timestamps.create(item3Timestamp, item3Timestamp).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1, itemHash3, itemHash2],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result.syncToken).toEqual('MjoxNjE2MTY0NjMzLjI0MTU3MQ==')
|
||||
expect(Buffer.from(result.syncToken, 'base64').toString('utf-8')).toEqual('2:1616164633.241571')
|
||||
})
|
||||
|
||||
it('should update existing items', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [],
|
||||
savedItems: [item1],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTMxMg==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark as skipped existing items that failed to update', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(item1)
|
||||
updateExistingItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip saving conflicting items and mark them as sync conflicts when saving fails', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
saveNewItemUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1, itemHash2],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash2,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip saving conflicting items and mark them as sync conflicts when saving throws an error', async () => {
|
||||
itemRepository.findByUuid = jest.fn().mockReturnValue(null)
|
||||
saveNewItemUseCase.execute = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
const result = await createService().saveItems({
|
||||
itemHashes: [itemHash1, itemHash2],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash1,
|
||||
},
|
||||
{
|
||||
type: 'uuid_conflict',
|
||||
unsavedItem: itemHash2,
|
||||
},
|
||||
],
|
||||
savedItems: [],
|
||||
syncToken: 'MjoxNjE2MTY0NjMzLjI0MTU2OQ==',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,239 +0,0 @@
|
||||
import { Time, TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { GetItemsDTO } from './GetItemsDTO'
|
||||
import { GetItemsResult } from './GetItemsResult'
|
||||
import { Item } from './Item'
|
||||
import { ItemConflict } from './ItemConflict'
|
||||
import { ItemQuery } from './ItemQuery'
|
||||
import { ItemRepositoryInterface } from './ItemRepositoryInterface'
|
||||
import { ItemServiceInterface } from './ItemServiceInterface'
|
||||
import { SaveItemsDTO } from './SaveItemsDTO'
|
||||
import { SaveItemsResult } from './SaveItemsResult'
|
||||
import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
|
||||
import { SaveNewItem } from '../UseCase/Syncing/SaveNewItem/SaveNewItem'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { UpdateExistingItem } from '../UseCase/Syncing/UpdateExistingItem/UpdateExistingItem'
|
||||
|
||||
export class ItemService implements ItemServiceInterface {
|
||||
private readonly DEFAULT_ITEMS_LIMIT = 150
|
||||
private readonly SYNC_TOKEN_VERSION = 2
|
||||
|
||||
constructor(
|
||||
private itemSaveValidator: ItemSaveValidatorInterface,
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private contentSizeTransferLimit: number,
|
||||
private itemTransferCalculator: ItemTransferCalculatorInterface,
|
||||
private timer: TimerInterface,
|
||||
private maxItemsSyncLimit: number,
|
||||
private saveNewItem: SaveNewItem,
|
||||
private updateExistingItem: UpdateExistingItem,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async getItems(dto: GetItemsDTO): Promise<GetItemsResult> {
|
||||
const lastSyncTime = this.getLastSyncTime(dto)
|
||||
const syncTimeComparison = dto.cursorToken ? '>=' : '>'
|
||||
const limit = dto.limit === undefined || dto.limit < 1 ? this.DEFAULT_ITEMS_LIMIT : dto.limit
|
||||
const upperBoundLimit = limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit
|
||||
|
||||
const itemQuery: ItemQuery = {
|
||||
userUuid: dto.userUuid,
|
||||
lastSyncTime,
|
||||
syncTimeComparison,
|
||||
contentType: dto.contentType,
|
||||
deleted: lastSyncTime ? undefined : false,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
limit: upperBoundLimit,
|
||||
}
|
||||
|
||||
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
||||
itemQuery,
|
||||
this.contentSizeTransferLimit,
|
||||
)
|
||||
let items: Array<Item> = []
|
||||
if (itemUuidsToFetch.length > 0) {
|
||||
items = await this.itemRepository.findAll({
|
||||
uuids: itemUuidsToFetch,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
}
|
||||
const totalItemsCount = await this.itemRepository.countAll(itemQuery)
|
||||
|
||||
let cursorToken = undefined
|
||||
if (totalItemsCount > upperBoundLimit) {
|
||||
const lastSyncTime = items[items.length - 1].props.timestamps.updatedAt / Time.MicrosecondsInASecond
|
||||
cursorToken = Buffer.from(`${this.SYNC_TOKEN_VERSION}:${lastSyncTime}`, 'utf-8').toString('base64')
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
cursorToken,
|
||||
}
|
||||
}
|
||||
|
||||
async saveItems(dto: SaveItemsDTO): Promise<SaveItemsResult> {
|
||||
const savedItems: Array<Item> = []
|
||||
const conflicts: Array<ItemConflict> = []
|
||||
|
||||
const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
for (const itemHash of dto.itemHashes) {
|
||||
if (dto.readOnlyAccess) {
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.ReadOnlyError,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const existingItem = await this.itemRepository.findByUuid(itemHash.uuid)
|
||||
const processingResult = await this.itemSaveValidator.validate({
|
||||
userUuid: dto.userUuid,
|
||||
apiVersion: dto.apiVersion,
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
if (!processingResult.passed) {
|
||||
if (processingResult.conflict) {
|
||||
conflicts.push(processingResult.conflict)
|
||||
}
|
||||
if (processingResult.skipped) {
|
||||
savedItems.push(processingResult.skipped)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (existingItem) {
|
||||
const udpatedItemOrError = await this.updateExistingItem.execute({
|
||||
existingItem,
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
})
|
||||
if (udpatedItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Updating item ${itemHash.uuid} failed. Error: ${udpatedItemOrError.getError()}`,
|
||||
)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
const updatedItem = udpatedItemOrError.getValue()
|
||||
|
||||
savedItems.push(updatedItem)
|
||||
} else {
|
||||
try {
|
||||
const newItemOrError = await this.saveNewItem.execute({
|
||||
userUuid: dto.userUuid,
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
})
|
||||
if (newItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Saving item ${itemHash.uuid} failed. Error: ${newItemOrError.getError()}`,
|
||||
)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
const newItem = newItemOrError.getValue()
|
||||
|
||||
savedItems.push(newItem)
|
||||
} catch (error) {
|
||||
this.logger.error(`[${dto.userUuid}] Saving item ${itemHash.uuid} failed. Error: ${(error as Error).message}`)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncToken = this.calculateSyncToken(lastUpdatedTimestamp, savedItems)
|
||||
|
||||
return {
|
||||
savedItems,
|
||||
conflicts,
|
||||
syncToken,
|
||||
}
|
||||
}
|
||||
|
||||
async frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>> {
|
||||
const itemsKeys = await this.itemRepository.findAll({
|
||||
userUuid,
|
||||
contentType: ContentType.TYPES.ItemsKey,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
|
||||
const retrievedItemsIds: Array<string> = retrievedItems.map((item: Item) => item.id.toString())
|
||||
|
||||
itemsKeys.forEach((itemKey: Item) => {
|
||||
if (retrievedItemsIds.indexOf(itemKey.id.toString()) === -1) {
|
||||
retrievedItems.unshift(itemKey)
|
||||
}
|
||||
})
|
||||
|
||||
return retrievedItems
|
||||
}
|
||||
|
||||
private calculateSyncToken(lastUpdatedTimestamp: number, savedItems: Array<Item>): string {
|
||||
if (savedItems.length) {
|
||||
const sortedItems = savedItems.sort((itemA: Item, itemB: Item) => {
|
||||
return itemA.props.timestamps.updatedAt > itemB.props.timestamps.updatedAt ? 1 : -1
|
||||
})
|
||||
lastUpdatedTimestamp = sortedItems[sortedItems.length - 1].props.timestamps.updatedAt
|
||||
}
|
||||
|
||||
const lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles = lastUpdatedTimestamp + 1
|
||||
|
||||
return Buffer.from(
|
||||
`${this.SYNC_TOKEN_VERSION}:${
|
||||
lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles / Time.MicrosecondsInASecond
|
||||
}`,
|
||||
'utf-8',
|
||||
).toString('base64')
|
||||
}
|
||||
|
||||
private getLastSyncTime(dto: GetItemsDTO): number | undefined {
|
||||
let token = dto.syncToken
|
||||
if (dto.cursorToken !== undefined && dto.cursorToken !== null) {
|
||||
token = dto.cursorToken
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const decodedToken = Buffer.from(token, 'base64').toString('utf-8')
|
||||
|
||||
const tokenParts = decodedToken.split(':')
|
||||
const version = tokenParts.shift()
|
||||
|
||||
switch (version) {
|
||||
case '1':
|
||||
return this.timer.convertStringDateToMicroseconds(tokenParts.join(':'))
|
||||
case '2':
|
||||
return +tokenParts[0] * Time.MicrosecondsInASecond
|
||||
default:
|
||||
throw Error('Sync token is missing version part')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { GetItemsDTO } from './GetItemsDTO'
|
||||
import { GetItemsResult } from './GetItemsResult'
|
||||
import { Item } from './Item'
|
||||
import { SaveItemsDTO } from './SaveItemsDTO'
|
||||
import { SaveItemsResult } from './SaveItemsResult'
|
||||
|
||||
export interface ItemServiceInterface {
|
||||
getItems(dto: GetItemsDTO): Promise<GetItemsResult>
|
||||
saveItems(dto: SaveItemsDTO): Promise<SaveItemsResult>
|
||||
frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>>
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Item } from './Item'
|
||||
import { ItemConflict } from './ItemConflict'
|
||||
|
||||
export type SaveItemsResult = {
|
||||
savedItems: Array<Item>
|
||||
conflicts: Array<ItemConflict>
|
||||
syncToken: string
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Item } from '../Item'
|
||||
|
||||
import { ContentFilter } from './ContentFilter'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ItemHash } from '../ItemHash'
|
||||
|
||||
describe('ContentFilter', () => {
|
||||
let existingItem: Item
|
||||
@@ -14,25 +15,25 @@ describe('ContentFilter', () => {
|
||||
const invalidContents = [[], { foo: 'bar' }, [{ foo: 'bar' }], 123, new Date(1)]
|
||||
|
||||
for (const invalidContent of invalidContents) {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: invalidContent as unknown as string,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: invalidContent as unknown as string,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
itemHash,
|
||||
existingItem: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: invalidContent,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'content_error',
|
||||
},
|
||||
})
|
||||
@@ -46,11 +47,14 @@ describe('ContentFilter', () => {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
itemHash: ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content: validContent as unknown as string,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue(),
|
||||
existingItem,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ import { ConflictType } from '@standardnotes/responses'
|
||||
|
||||
export class ContentFilter implements ItemSaveRuleInterface {
|
||||
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
|
||||
if (dto.itemHash.content === undefined || dto.itemHash.content === null) {
|
||||
if (dto.itemHash.props.content === undefined || dto.itemHash.props.content === null) {
|
||||
return {
|
||||
passed: true,
|
||||
}
|
||||
}
|
||||
|
||||
const validContent = typeof dto.itemHash.content === 'string'
|
||||
const validContent = typeof dto.itemHash.props.content === 'string'
|
||||
|
||||
if (!validContent) {
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ApiVersion } from '../../Api/ApiVersion'
|
||||
import { Item } from '../Item'
|
||||
|
||||
import { ContentTypeFilter } from './ContentTypeFilter'
|
||||
import { ItemHash } from '../ItemHash'
|
||||
|
||||
describe('ContentTypeFilter', () => {
|
||||
let existingItem: Item
|
||||
@@ -22,23 +23,24 @@ describe('ContentTypeFilter', () => {
|
||||
]
|
||||
|
||||
for (const invalidContentType of invalidContentTypes) {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: invalidContentType,
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: invalidContentType,
|
||||
},
|
||||
itemHash,
|
||||
existingItem: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: invalidContentType,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'content_type_error',
|
||||
},
|
||||
})
|
||||
@@ -49,13 +51,18 @@ describe('ContentTypeFilter', () => {
|
||||
const validContentTypes = ['Note', 'SN|ItemsKey', 'SN|Component', 'SN|Editor', 'SN|ExtensionRepo', 'Tag']
|
||||
|
||||
for (const validContentType of validContentTypes) {
|
||||
const itemHash = ItemHash.create({
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: validContentType,
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '123e4567-e89b-12d3-a456-426655440000',
|
||||
content_type: validContentType,
|
||||
},
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
|
||||
|
||||
export class ContentTypeFilter implements ItemSaveRuleInterface {
|
||||
async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.props.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return {
|
||||
passed: false,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Item } from '../Item'
|
||||
|
||||
import { OwnershipFilter } from './OwnershipFilter'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { ItemHash } from '../ItemHash'
|
||||
|
||||
describe('OwnershipFilter', () => {
|
||||
let existingItem: Item
|
||||
@@ -30,23 +31,29 @@ describe('OwnershipFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items belonging to a different user', 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: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000001',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'uuid_conflict',
|
||||
},
|
||||
})
|
||||
@@ -56,10 +63,18 @@ describe('OwnershipFilter', () => {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -72,10 +87,18 @@ describe('OwnershipFilter', () => {
|
||||
const result = await createFilter().check({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
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: null,
|
||||
})
|
||||
|
||||
@@ -85,23 +108,29 @@ describe('OwnershipFilter', () => {
|
||||
})
|
||||
|
||||
it('should return an error if the user uuid is invalid', 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: null,
|
||||
}).getValue()
|
||||
const result = await createFilter().check({
|
||||
userUuid: 'invalid',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
itemHash: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
passed: false,
|
||||
conflict: {
|
||||
unsavedItem: {
|
||||
uuid: '2-3-4',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
},
|
||||
unsavedItem: itemHash,
|
||||
type: 'uuid_error',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -42,8 +42,11 @@ describe('TimeDifferenceFilter', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash = {
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '1-2-3',
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
@@ -57,7 +60,7 @@ describe('TimeDifferenceFilter', () => {
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
}).getValue()
|
||||
})
|
||||
|
||||
it('should leave non existing items', async () => {
|
||||
@@ -74,8 +77,11 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items from legacy clients', async () => {
|
||||
delete itemHash.updated_at
|
||||
delete itemHash.updated_at_timestamp
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: undefined,
|
||||
updated_at_timestamp: undefined,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -90,7 +96,10 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items having update at timestamp different in microseconds precision', async () => {
|
||||
itemHash.updated_at_timestamp = existingItem.props.timestamps.updatedAt + 1
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at_timestamp: existingItem.props.timestamps.updatedAt + 1,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -109,7 +118,10 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items having update at timestamp same in microseconds precision', async () => {
|
||||
itemHash.updated_at_timestamp = existingItem.props.timestamps.updatedAt
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at_timestamp: existingItem.props.timestamps.updatedAt,
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -124,14 +136,17 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items having update at timestamp different by a second for legacy clients', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInASecond +
|
||||
1,
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInASecond +
|
||||
1,
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -150,10 +165,13 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items having update at timestamp different by less then a second for legacy clients', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -168,14 +186,17 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should filter out items having update at timestamp different by a millisecond', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInAMillisecond +
|
||||
1,
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
new Date(
|
||||
timeHelper.convertMicrosecondsToMilliseconds(existingItem.props.timestamps.updatedAt) +
|
||||
Time.MicrosecondsInAMillisecond +
|
||||
1,
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
@@ -194,10 +215,13 @@ describe('TimeDifferenceFilter', () => {
|
||||
})
|
||||
|
||||
it('should leave items having update at timestamp different by less than a millisecond', async () => {
|
||||
itemHash.updated_at = timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
)
|
||||
itemHash = ItemHash.create({
|
||||
...itemHash.props,
|
||||
updated_at: timeHelper.formatDate(
|
||||
timeHelper.convertMicrosecondsToDate(existingItem.props.timestamps.updatedAt),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
}).getValue()
|
||||
|
||||
const result = await createFilter().check({
|
||||
userUuid: '1-2-3',
|
||||
|
||||
@@ -17,11 +17,11 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface {
|
||||
}
|
||||
}
|
||||
|
||||
let incomingUpdatedAtTimestamp = dto.itemHash.updated_at_timestamp
|
||||
let incomingUpdatedAtTimestamp = dto.itemHash.props.updated_at_timestamp
|
||||
if (incomingUpdatedAtTimestamp === undefined) {
|
||||
incomingUpdatedAtTimestamp =
|
||||
dto.itemHash.updated_at !== undefined
|
||||
? this.timer.convertStringDateToMicroseconds(dto.itemHash.updated_at)
|
||||
dto.itemHash.props.updated_at !== undefined
|
||||
? this.timer.convertStringDateToMicroseconds(dto.itemHash.props.updated_at)
|
||||
: this.timer.convertStringDateToMicroseconds(new Date(0).toString())
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class TimeDifferenceFilter implements ItemSaveRuleInterface {
|
||||
}
|
||||
|
||||
private itemHashHasMicrosecondsPrecision(itemHash: ItemHash) {
|
||||
return itemHash.updated_at_timestamp !== undefined
|
||||
return itemHash.props.updated_at_timestamp !== undefined
|
||||
}
|
||||
|
||||
private getMinimalConflictIntervalMicroseconds(apiVersion?: string): number {
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { MessageProps } from './MessageProps'
|
||||
|
||||
export class Message extends Entity<MessageProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: MessageProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { NotificationProps } from './NotificationProps'
|
||||
|
||||
export class Notification extends Entity<NotificationProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: NotificationProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItem } from './SharedVaultItem'
|
||||
|
||||
describe('SharedVaultItem', () => {
|
||||
it('should create an entity', () => {
|
||||
const entityOrError = SharedVaultItem.create({
|
||||
itemId: new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
sharedVaultId: new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
keySystemIdentifier: 'key-system-identifier',
|
||||
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123456789, 123456789).getValue(),
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
expect(entityOrError.getValue().id).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItemProps } from './SharedVaultItemProps'
|
||||
|
||||
export class SharedVaultItem extends Entity<SharedVaultItemProps> {
|
||||
private constructor(props: SharedVaultItemProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
static create(props: SharedVaultItemProps, id?: UniqueEntityId): Result<SharedVaultItem> {
|
||||
return Result.ok<SharedVaultItem>(new SharedVaultItem(props, id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export interface SharedVaultItemProps {
|
||||
sharedVaultId: UniqueEntityId
|
||||
itemId: UniqueEntityId
|
||||
keySystemIdentifier: string
|
||||
lastEditedBy: Uuid
|
||||
timestamps: Timestamps
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItem } from './SharedVaultItem'
|
||||
|
||||
export interface SharedVaultItemRepositoryInterface {
|
||||
save(sharedVaultItem: SharedVaultItem): Promise<void>
|
||||
findBySharedVaultId(sharedVaultId: UniqueEntityId): Promise<SharedVaultItem[]>
|
||||
}
|
||||
@@ -3,12 +3,13 @@ import { Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { SharedVault } from './SharedVault'
|
||||
|
||||
describe('SharedVault', () => {
|
||||
it('should create an entity', () => {
|
||||
it('should create an aggregate', () => {
|
||||
const entityOrError = SharedVault.create({
|
||||
fileUploadBytesLimit: 1_000_000,
|
||||
fileUploadBytesUsed: 0,
|
||||
timestamps: Timestamps.create(123456789, 123456789).getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultItems: [],
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { Aggregate, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultProps } from './SharedVaultProps'
|
||||
|
||||
export class SharedVault extends Entity<SharedVaultProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
export class SharedVault extends Aggregate<SharedVaultProps> {
|
||||
private constructor(props: SharedVaultProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Uuid, Timestamps } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItem } from './Item/SharedVaultItem'
|
||||
|
||||
export interface SharedVaultProps {
|
||||
userUuid: Uuid
|
||||
fileUploadBytesUsed: number
|
||||
fileUploadBytesLimit: number
|
||||
timestamps: Timestamps
|
||||
sharedVaultItems: SharedVaultItem[]
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { SharedVaultInviteProps } from './SharedVaultInviteProps'
|
||||
|
||||
export class SharedVaultInvite extends Entity<SharedVaultInviteProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: SharedVaultInviteProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { SharedVaultUserProps } from './SharedVaultUserProps'
|
||||
|
||||
export class SharedVaultUser extends Entity<SharedVaultUserProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: SharedVaultUserProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResu
|
||||
fileUploadBytesUsed: 0,
|
||||
userUuid,
|
||||
timestamps,
|
||||
sharedVaultItems: [],
|
||||
})
|
||||
if (sharedVaultOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultOrError.getError())
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ describe('DeleteSharedVault', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
@@ -115,6 +116,7 @@ describe('DeleteSharedVault', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
const useCase = createUseCase()
|
||||
|
||||
+2
@@ -20,6 +20,7 @@ describe('GetSharedVaultUsers', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
@@ -66,6 +67,7 @@ describe('GetSharedVaultUsers', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ describe('GetSharedVaults', () => {
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
fileUploadBytesLimit: 123,
|
||||
fileUploadBytesUsed: 123,
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
sharedVaultRepository.findByUuids = jest.fn().mockResolvedValue([sharedVault])
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ describe('InviteUserToSharedVault', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
@@ -152,6 +153,7 @@ describe('InviteUserToSharedVault', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('10000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ describe('RemoveUserFromSharedVault', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
@@ -88,6 +89,7 @@ describe('RemoveUserFromSharedVault', () => {
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
sharedVaultItems: [],
|
||||
}).getValue()
|
||||
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalculatorInterface'
|
||||
import { GetItems } from './GetItems'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ContentType, Dates, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
describe('GetItems', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
const contentSizeTransferLimit = 100
|
||||
let itemTransferCalculator: ItemTransferCalculatorInterface
|
||||
let timer: TimerInterface
|
||||
const maxItemsSyncLimit = 100
|
||||
let item: Item
|
||||
|
||||
const createUseCase = () =>
|
||||
new GetItems(itemRepository, contentSizeTransferLimit, itemTransferCalculator, timer, maxItemsSyncLimit)
|
||||
|
||||
beforeEach(() => {
|
||||
item = 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()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockResolvedValue([item])
|
||||
itemRepository.countAll = jest.fn().mockResolvedValue(1)
|
||||
|
||||
itemTransferCalculator = {} as jest.Mocked<ItemTransferCalculatorInterface>
|
||||
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockResolvedValue(['item-uuid'])
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(123)
|
||||
})
|
||||
|
||||
it('returns items', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'user-uuid',
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
items: [item],
|
||||
cursorToken: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return cursor token if there are more items to fetch', async () => {
|
||||
itemRepository.countAll = jest.fn().mockResolvedValue(101)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'user-uuid',
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
items: [item],
|
||||
cursorToken: 'MjowLjAwMDEyMw==',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return items based on the cursort token passed', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'user-uuid',
|
||||
cursorToken: 'MjowLjAwMDEyMw==',
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
items: [item],
|
||||
cursorToken: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return items based on a sync token containing string date', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const syncTokenData = '1:2021-01-01T00:00:00.000Z'
|
||||
const syncToken = Buffer.from(syncTokenData, 'utf-8').toString('base64')
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'user-uuid',
|
||||
syncToken,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
items: [item],
|
||||
cursorToken: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error if the sync token is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const syncTokenData = 'invalid'
|
||||
const syncToken = Buffer.from(syncTokenData, 'utf-8').toString('base64')
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'user-uuid',
|
||||
syncToken,
|
||||
contentType: undefined,
|
||||
limit: undefined,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual('Sync token is missing version part')
|
||||
})
|
||||
|
||||
it('should guard the upper bound limit of items to fetch', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'user-uuid',
|
||||
cursorToken: undefined,
|
||||
contentType: undefined,
|
||||
limit: 200,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
items: [item],
|
||||
cursorToken: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { Time, TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { GetItemsResult } from './GetItemsResult'
|
||||
import { ItemQuery } from '../../../Item/ItemQuery'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemTransferCalculatorInterface } from '../../../Item/ItemTransferCalculatorInterface'
|
||||
import { GetItemsDTO } from './GetItemsDTO'
|
||||
|
||||
export class GetItems implements UseCaseInterface<GetItemsResult> {
|
||||
private readonly DEFAULT_ITEMS_LIMIT = 150
|
||||
private readonly SYNC_TOKEN_VERSION = 2
|
||||
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private contentSizeTransferLimit: number,
|
||||
private itemTransferCalculator: ItemTransferCalculatorInterface,
|
||||
private timer: TimerInterface,
|
||||
private maxItemsSyncLimit: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetItemsDTO): Promise<Result<GetItemsResult>> {
|
||||
const lastSyncTimeOrError = this.getLastSyncTime(dto)
|
||||
if (lastSyncTimeOrError.isFailed()) {
|
||||
return Result.fail(lastSyncTimeOrError.getError())
|
||||
}
|
||||
const lastSyncTime = lastSyncTimeOrError.getValue()
|
||||
|
||||
const syncTimeComparison = dto.cursorToken ? '>=' : '>'
|
||||
const limit = dto.limit === undefined || dto.limit < 1 ? this.DEFAULT_ITEMS_LIMIT : dto.limit
|
||||
const upperBoundLimit = limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit
|
||||
|
||||
const itemQuery: ItemQuery = {
|
||||
userUuid: dto.userUuid,
|
||||
lastSyncTime: lastSyncTime ?? undefined,
|
||||
syncTimeComparison,
|
||||
contentType: dto.contentType,
|
||||
deleted: lastSyncTime ? undefined : false,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
limit: upperBoundLimit,
|
||||
}
|
||||
|
||||
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
||||
itemQuery,
|
||||
this.contentSizeTransferLimit,
|
||||
)
|
||||
let items: Array<Item> = []
|
||||
if (itemUuidsToFetch.length > 0) {
|
||||
items = await this.itemRepository.findAll({
|
||||
uuids: itemUuidsToFetch,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
}
|
||||
const totalItemsCount = await this.itemRepository.countAll(itemQuery)
|
||||
|
||||
let cursorToken = undefined
|
||||
if (totalItemsCount > upperBoundLimit) {
|
||||
const lastSyncTime = items[items.length - 1].props.timestamps.updatedAt / Time.MicrosecondsInASecond
|
||||
cursorToken = Buffer.from(`${this.SYNC_TOKEN_VERSION}:${lastSyncTime}`, 'utf-8').toString('base64')
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
items,
|
||||
cursorToken,
|
||||
})
|
||||
}
|
||||
|
||||
private getLastSyncTime(dto: GetItemsDTO): Result<number | null> {
|
||||
let token = dto.syncToken
|
||||
if (dto.cursorToken !== undefined && dto.cursorToken !== null) {
|
||||
token = dto.cursorToken
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return Result.ok(null)
|
||||
}
|
||||
|
||||
const decodedToken = Buffer.from(token, 'base64').toString('utf-8')
|
||||
|
||||
const tokenParts = decodedToken.split(':')
|
||||
const version = tokenParts.shift()
|
||||
|
||||
switch (version) {
|
||||
case '1':
|
||||
return Result.ok(this.timer.convertStringDateToMicroseconds(tokenParts.join(':')))
|
||||
case '2':
|
||||
return Result.ok(+tokenParts[0] * Time.MicrosecondsInASecond)
|
||||
default:
|
||||
return Result.fail('Sync token is missing version part')
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export type GetItemsDTO = {
|
||||
export interface GetItemsDTO {
|
||||
userUuid: string
|
||||
syncToken?: string | null
|
||||
cursorToken?: string | null
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Item } from '../../../Item/Item'
|
||||
|
||||
export interface GetItemsResult {
|
||||
items: Item[]
|
||||
cursorToken?: string
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemSaveValidatorInterface } from '../../../Item/SaveValidator/ItemSaveValidatorInterface'
|
||||
import { SaveItems } from './SaveItems'
|
||||
import { SaveNewItem } from '../SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
|
||||
import { Logger } from 'winston'
|
||||
import { ContentType, Dates, Result, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { Item } from '../../../Item/Item'
|
||||
|
||||
describe('SaveItems', () => {
|
||||
let itemSaveValidator: ItemSaveValidatorInterface
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let timer: TimerInterface
|
||||
let saveNewItem: SaveNewItem
|
||||
let updateExistingItem: UpdateExistingItem
|
||||
let logger: Logger
|
||||
let itemHash1: ItemHash
|
||||
let savedItem: Item
|
||||
|
||||
const createUseCase = () =>
|
||||
new SaveItems(itemSaveValidator, itemRepository, timer, saveNewItem, updateExistingItem, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
itemSaveValidator = {} as jest.Mocked<ItemSaveValidatorInterface>
|
||||
itemSaveValidator.validate = jest.fn().mockResolvedValue({ passed: true })
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findByUuid = jest.fn().mockResolvedValue(null)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
|
||||
savedItem = 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()
|
||||
|
||||
saveNewItem = {} as jest.Mocked<SaveNewItem>
|
||||
saveNewItem.execute = jest.fn().mockReturnValue(Result.ok(savedItem))
|
||||
|
||||
updateExistingItem = {} as jest.Mocked<UpdateExistingItem>
|
||||
updateExistingItem.execute = jest.fn().mockResolvedValue(Result.ok(savedItem))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
|
||||
itemHash1 = ItemHash.create({
|
||||
uuid: 'item-uuid',
|
||||
user_uuid: 'user-uuid',
|
||||
content: 'content',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
deleted: false,
|
||||
auth_hash: 'auth-hash',
|
||||
enc_item_key: 'enc-item-key',
|
||||
items_key_id: 'items-key-id',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
created_at: '2020-01-01T00:00:00.000Z',
|
||||
created_at_timestamp: 123,
|
||||
updated_at: '2020-01-01T00:00:00.000Z',
|
||||
updated_at_timestamp: 123,
|
||||
}).getValue()
|
||||
})
|
||||
|
||||
it('should save new items', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().syncToken).toEqual('MjowLjAwMDEyNA==')
|
||||
expect(saveNewItem.execute).toHaveBeenCalledWith({
|
||||
itemHash: itemHash1,
|
||||
userUuid: 'user-uuid',
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark items as conflicts if saving new item fails', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
saveNewItem.execute = jest.fn().mockResolvedValue(Result.fail('error'))
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().conflicts).toEqual([
|
||||
{
|
||||
unsavedItem: itemHash1,
|
||||
type: 'uuid_conflict',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should mark items as conflicts if saving new item throws an error', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
saveNewItem.execute = jest.fn().mockRejectedValue(new Error('error'))
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().conflicts).toEqual([
|
||||
{
|
||||
unsavedItem: itemHash1,
|
||||
type: 'uuid_conflict',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should not save items if in read-only mode', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: true,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(saveNewItem.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return conflicts if the items have not passed validation', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const conflict = {
|
||||
unsavedItem: itemHash1,
|
||||
type: 'conflict-type',
|
||||
}
|
||||
itemSaveValidator.validate = jest.fn().mockResolvedValue({ passed: false, conflict })
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().conflicts).toEqual([conflict])
|
||||
})
|
||||
|
||||
it('should mark items as saved if they are skipped on validation', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemSaveValidator.validate = jest.fn().mockResolvedValue({ passed: false, skipped: savedItem })
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().savedItems).toEqual([savedItem])
|
||||
})
|
||||
|
||||
it('should update existing items', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemRepository.findByUuid = jest.fn().mockResolvedValue(savedItem)
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(updateExistingItem.execute).toHaveBeenCalledWith({
|
||||
itemHash: itemHash1,
|
||||
existingItem: savedItem,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark items as conflicts if updating existing item fails', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemRepository.findByUuid = jest.fn().mockResolvedValue(savedItem)
|
||||
updateExistingItem.execute = jest.fn().mockResolvedValue(Result.fail('error'))
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [itemHash1],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '1',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().conflicts).toEqual([
|
||||
{
|
||||
unsavedItem: itemHash1,
|
||||
type: 'uuid_conflict',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should calculate the sync token based on existing and new items saved', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
saveNewItem.execute = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(Result.ok(savedItem))
|
||||
.mockResolvedValueOnce(
|
||||
Result.ok(
|
||||
Item.create({
|
||||
...savedItem.props,
|
||||
timestamps: Timestamps.create(100, 100).getValue(),
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
Result.ok(
|
||||
Item.create({
|
||||
...savedItem.props,
|
||||
timestamps: Timestamps.create(159, 159).getValue(),
|
||||
}).getValue(),
|
||||
),
|
||||
)
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemHashes: [
|
||||
itemHash1,
|
||||
ItemHash.create({ ...itemHash1.props, uuid: 'item-uuid-2' }).getValue(),
|
||||
ItemHash.create({ ...itemHash1.props, uuid: 'item-uuid-2' }).getValue(),
|
||||
],
|
||||
userUuid: 'user-uuid',
|
||||
apiVersion: '2',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: 'session-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue().syncToken).toEqual('MjowLjAwMDE2')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { SaveItemsResult } from './SaveItemsResult'
|
||||
import { SaveItemsDTO } from './SaveItemsDTO'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemConflict } from '../../../Item/ItemConflict'
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
import { Time, TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { ItemSaveValidatorInterface } from '../../../Item/SaveValidator/ItemSaveValidatorInterface'
|
||||
import { SaveNewItem } from '../SaveNewItem/SaveNewItem'
|
||||
import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
|
||||
|
||||
export class SaveItems implements UseCaseInterface<SaveItemsResult> {
|
||||
private readonly SYNC_TOKEN_VERSION = 2
|
||||
|
||||
constructor(
|
||||
private itemSaveValidator: ItemSaveValidatorInterface,
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private saveNewItem: SaveNewItem,
|
||||
private updateExistingItem: UpdateExistingItem,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: SaveItemsDTO): Promise<Result<SaveItemsResult>> {
|
||||
const savedItems: Array<Item> = []
|
||||
const conflicts: Array<ItemConflict> = []
|
||||
|
||||
const lastUpdatedTimestamp = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
for (const itemHash of dto.itemHashes) {
|
||||
if (dto.readOnlyAccess) {
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.ReadOnlyError,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const existingItem = await this.itemRepository.findByUuid(itemHash.props.uuid)
|
||||
const processingResult = await this.itemSaveValidator.validate({
|
||||
userUuid: dto.userUuid,
|
||||
apiVersion: dto.apiVersion,
|
||||
itemHash,
|
||||
existingItem,
|
||||
})
|
||||
if (!processingResult.passed) {
|
||||
if (processingResult.conflict) {
|
||||
conflicts.push(processingResult.conflict)
|
||||
}
|
||||
if (processingResult.skipped) {
|
||||
savedItems.push(processingResult.skipped)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (existingItem) {
|
||||
const udpatedItemOrError = await this.updateExistingItem.execute({
|
||||
existingItem,
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
})
|
||||
if (udpatedItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Updating item ${itemHash.props.uuid} failed. Error: ${udpatedItemOrError.getError()}`,
|
||||
)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
const updatedItem = udpatedItemOrError.getValue()
|
||||
|
||||
savedItems.push(updatedItem)
|
||||
} else {
|
||||
try {
|
||||
const newItemOrError = await this.saveNewItem.execute({
|
||||
userUuid: dto.userUuid,
|
||||
itemHash,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
})
|
||||
if (newItemOrError.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Saving item ${itemHash.props.uuid} failed. Error: ${newItemOrError.getError()}`,
|
||||
)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
const newItem = newItemOrError.getValue()
|
||||
|
||||
savedItems.push(newItem)
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[${dto.userUuid}] Saving item ${itemHash.props.uuid} failed. Error: ${(error as Error).message}`,
|
||||
)
|
||||
|
||||
conflicts.push({
|
||||
unsavedItem: itemHash,
|
||||
type: ConflictType.UuidConflict,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncToken = this.calculateSyncToken(lastUpdatedTimestamp, savedItems)
|
||||
|
||||
return Result.ok({
|
||||
savedItems,
|
||||
conflicts,
|
||||
syncToken,
|
||||
})
|
||||
}
|
||||
|
||||
private calculateSyncToken(lastUpdatedTimestamp: number, savedItems: Array<Item>): string {
|
||||
if (savedItems.length) {
|
||||
const sortedItems = savedItems.sort((itemA: Item, itemB: Item) => {
|
||||
return itemA.props.timestamps.updatedAt > itemB.props.timestamps.updatedAt ? 1 : -1
|
||||
})
|
||||
lastUpdatedTimestamp = sortedItems[sortedItems.length - 1].props.timestamps.updatedAt
|
||||
}
|
||||
|
||||
const lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles = lastUpdatedTimestamp + 1
|
||||
|
||||
return Buffer.from(
|
||||
`${this.SYNC_TOKEN_VERSION}:${
|
||||
lastUpdatedTimestampWithMicrosecondPreventingSyncDoubles / Time.MicrosecondsInASecond
|
||||
}`,
|
||||
'utf-8',
|
||||
).toString('base64')
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { ItemHash } from './ItemHash'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
|
||||
export type SaveItemsDTO = {
|
||||
export interface SaveItemsDTO {
|
||||
itemHashes: ItemHash[]
|
||||
userUuid: string
|
||||
apiVersion: string
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemConflict } from '../../../Item/ItemConflict'
|
||||
|
||||
export interface SaveItemsResult {
|
||||
savedItems: Item[]
|
||||
conflicts: ItemConflict[]
|
||||
syncToken: string
|
||||
}
|
||||
+33
-12
@@ -37,8 +37,11 @@ describe('SaveNewItem', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash1 = {
|
||||
itemHash1 = ItemHash.create({
|
||||
uuid: '1-2-3',
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
@@ -52,7 +55,7 @@ describe('SaveNewItem', () => {
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
}).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.save = jest.fn()
|
||||
@@ -91,10 +94,13 @@ describe('SaveNewItem', () => {
|
||||
it('saves a new empty item', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.content = undefined
|
||||
itemHash1.content_type = null
|
||||
itemHash1.enc_item_key = undefined
|
||||
itemHash1.items_key_id = undefined
|
||||
itemHash1 = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
content: undefined,
|
||||
content_type: null,
|
||||
enc_item_key: undefined,
|
||||
items_key_id: undefined,
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -109,8 +115,11 @@ describe('SaveNewItem', () => {
|
||||
it('saves a new item with given timestamps', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.created_at_timestamp = 123
|
||||
itemHash1.updated_at_timestamp = 123
|
||||
itemHash1 = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -124,7 +133,10 @@ describe('SaveNewItem', () => {
|
||||
it('publishes a duplicate item synced event if the item is a duplicate', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.duplicate_of = '00000000-0000-0000-0000-000000000003'
|
||||
itemHash1 = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
duplicate_of: '00000000-0000-0000-0000-000000000003',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -140,7 +152,10 @@ describe('SaveNewItem', () => {
|
||||
it('publishes a item revision creation requested event if the item is a revision', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.updated_at = '2021-03-19T17:17:13.241Z'
|
||||
itemHash1 = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
updated_at: '2021-03-19T17:17:13.241Z',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -199,7 +214,10 @@ describe('SaveNewItem', () => {
|
||||
it('returns a failure if the content type is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.content_type = 'invalid'
|
||||
itemHash1 = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
content_type: 'invalid',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
@@ -213,7 +231,10 @@ describe('SaveNewItem', () => {
|
||||
it('returns a failure if the duplicate uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
itemHash1.duplicate_of = 'invalid'
|
||||
itemHash1 = ItemHash.create({
|
||||
...itemHash1.props,
|
||||
duplicate_of: 'invalid',
|
||||
}).getValue()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
|
||||
@@ -38,15 +38,15 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.props.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return Result.fail(contentTypeOrError.getError())
|
||||
}
|
||||
const contentType = contentTypeOrError.getValue()
|
||||
|
||||
let duplicateOf = null
|
||||
if (dto.itemHash.duplicate_of) {
|
||||
const duplicateOfOrError = Uuid.create(dto.itemHash.duplicate_of)
|
||||
if (dto.itemHash.props.duplicate_of) {
|
||||
const duplicateOfOrError = Uuid.create(dto.itemHash.props.duplicate_of)
|
||||
if (duplicateOfOrError.isFailed()) {
|
||||
return Result.fail(duplicateOfOrError.getError())
|
||||
}
|
||||
@@ -58,12 +58,12 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
|
||||
let createdAtDate = nowDate
|
||||
let createdAtTimestamp = now
|
||||
if (dto.itemHash.created_at_timestamp) {
|
||||
createdAtTimestamp = dto.itemHash.created_at_timestamp
|
||||
if (dto.itemHash.props.created_at_timestamp) {
|
||||
createdAtTimestamp = dto.itemHash.props.created_at_timestamp
|
||||
createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
|
||||
} else if (dto.itemHash.created_at) {
|
||||
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
|
||||
createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.created_at)
|
||||
} else if (dto.itemHash.props.created_at) {
|
||||
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.props.created_at)
|
||||
createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.props.created_at)
|
||||
}
|
||||
|
||||
const datesOrError = Dates.create(createdAtDate, nowDate)
|
||||
@@ -81,18 +81,18 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
const itemOrError = Item.create(
|
||||
{
|
||||
updatedWithSession,
|
||||
content: dto.itemHash.content ?? null,
|
||||
content: dto.itemHash.props.content ?? null,
|
||||
userUuid,
|
||||
contentType,
|
||||
encItemKey: dto.itemHash.enc_item_key ?? null,
|
||||
authHash: dto.itemHash.auth_hash ?? null,
|
||||
itemsKeyId: dto.itemHash.items_key_id ?? null,
|
||||
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.deleted ?? false,
|
||||
deleted: dto.itemHash.props.deleted ?? false,
|
||||
dates,
|
||||
timestamps,
|
||||
},
|
||||
new UniqueEntityId(dto.itemHash.uuid),
|
||||
new UniqueEntityId(dto.itemHash.props.uuid),
|
||||
)
|
||||
if (itemOrError.isFailed()) {
|
||||
return Result.fail(itemOrError.getError())
|
||||
|
||||
@@ -3,19 +3,23 @@ import 'reflect-metadata'
|
||||
import { ApiVersion } from '../../../Api/ApiVersion'
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemHash } from '../../../Item/ItemHash'
|
||||
import { ItemServiceInterface } from '../../../Item/ItemServiceInterface'
|
||||
|
||||
import { SyncItems } from './SyncItems'
|
||||
import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { GetItems } from '../GetItems/GetItems'
|
||||
import { SaveItems } from '../SaveItems/SaveItems'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
|
||||
describe('SyncItems', () => {
|
||||
let itemService: ItemServiceInterface
|
||||
let getItemsUseCase: GetItems
|
||||
let saveItemsUseCase: SaveItems
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let item1: Item
|
||||
let item2: Item
|
||||
let item3: Item
|
||||
let itemHash: ItemHash
|
||||
|
||||
const createUseCase = () => new SyncItems(itemService)
|
||||
const createUseCase = () => new SyncItems(itemRepository, getItemsUseCase, saveItemsUseCase)
|
||||
|
||||
beforeEach(() => {
|
||||
item1 = Item.create(
|
||||
@@ -67,8 +71,11 @@ describe('SyncItems', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000003'),
|
||||
).getValue()
|
||||
|
||||
itemHash = {
|
||||
itemHash = ItemHash.create({
|
||||
uuid: '2-3-4',
|
||||
user_uuid: '1-2-3',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
content: 'asdqwe',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
@@ -76,19 +83,27 @@ describe('SyncItems', () => {
|
||||
items_key_id: 'asdasd',
|
||||
created_at: '2021-02-19T11:35:45.655Z',
|
||||
updated_at: '2021-03-25T09:37:37.944Z',
|
||||
}
|
||||
}).getValue()
|
||||
|
||||
itemService = {} as jest.Mocked<ItemServiceInterface>
|
||||
itemService.getItems = jest.fn().mockReturnValue({
|
||||
items: [item1],
|
||||
cursorToken: 'asdzxc',
|
||||
})
|
||||
itemService.saveItems = jest.fn().mockReturnValue({
|
||||
savedItems: [item2],
|
||||
conflicts: [],
|
||||
syncToken: 'qwerty',
|
||||
})
|
||||
itemService.frontLoadKeysItemsToTop = jest.fn().mockReturnValue([item3, item1])
|
||||
getItemsUseCase = {} as jest.Mocked<GetItems>
|
||||
getItemsUseCase.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
items: [item1],
|
||||
cursorToken: 'asdzxc',
|
||||
}),
|
||||
)
|
||||
|
||||
saveItemsUseCase = {} as jest.Mocked<SaveItems>
|
||||
saveItemsUseCase.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
savedItems: [item2],
|
||||
conflicts: [],
|
||||
syncToken: 'qwerty',
|
||||
}),
|
||||
)
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item3, item1])
|
||||
})
|
||||
|
||||
it('should sync items', async () => {
|
||||
@@ -113,15 +128,14 @@ describe('SyncItems', () => {
|
||||
syncToken: 'qwerty',
|
||||
})
|
||||
|
||||
expect(itemService.frontLoadKeysItemsToTop).not.toHaveBeenCalled()
|
||||
expect(itemService.getItems).toHaveBeenCalledWith({
|
||||
expect(getItemsUseCase.execute).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
cursorToken: 'bar',
|
||||
limit: 10,
|
||||
syncToken: 'foo',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
expect(itemService.saveItems).toHaveBeenCalledWith({
|
||||
expect(saveItemsUseCase.execute).toHaveBeenCalledWith({
|
||||
itemHashes: [itemHash],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: '20200115',
|
||||
@@ -152,25 +166,29 @@ describe('SyncItems', () => {
|
||||
})
|
||||
|
||||
it('should sync items and return filtered out sync conflicts for consecutive sync operations', async () => {
|
||||
itemService.getItems = jest.fn().mockReturnValue({
|
||||
items: [item1, item2],
|
||||
cursorToken: 'asdzxc',
|
||||
})
|
||||
getItemsUseCase.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
items: [item1, item2],
|
||||
cursorToken: 'asdzxc',
|
||||
}),
|
||||
)
|
||||
|
||||
itemService.saveItems = jest.fn().mockReturnValue({
|
||||
savedItems: [],
|
||||
conflicts: [
|
||||
{
|
||||
serverItem: item2,
|
||||
type: 'sync_conflict',
|
||||
},
|
||||
{
|
||||
serverItem: undefined,
|
||||
type: 'sync_conflict',
|
||||
},
|
||||
],
|
||||
syncToken: 'qwerty',
|
||||
})
|
||||
saveItemsUseCase.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
savedItems: [],
|
||||
conflicts: [
|
||||
{
|
||||
serverItem: item2,
|
||||
type: 'sync_conflict',
|
||||
},
|
||||
{
|
||||
serverItem: undefined,
|
||||
type: 'sync_conflict',
|
||||
},
|
||||
],
|
||||
syncToken: 'qwerty',
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
@@ -203,4 +221,44 @@ describe('SyncItems', () => {
|
||||
syncToken: 'qwerty',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error if get items fails', async () => {
|
||||
getItemsUseCase.execute = jest.fn().mockReturnValue(Result.fail('error'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemHashes: [itemHash],
|
||||
computeIntegrityHash: false,
|
||||
syncToken: 'foo',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
cursorToken: 'bar',
|
||||
limit: 10,
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if save items fails', async () => {
|
||||
saveItemsUseCase.execute = jest.fn().mockReturnValue(Result.fail('error'))
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemHashes: [itemHash],
|
||||
computeIntegrityHash: false,
|
||||
syncToken: 'foo',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
cursorToken: 'bar',
|
||||
limit: 10,
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
snjsVersion: '1.2.3',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,34 +1,48 @@
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { Item } from '../../../Item/Item'
|
||||
import { ItemConflict } from '../../../Item/ItemConflict'
|
||||
import { ItemServiceInterface } from '../../../Item/ItemServiceInterface'
|
||||
import { SyncItemsDTO } from './SyncItemsDTO'
|
||||
import { SyncItemsResponse } from './SyncItemsResponse'
|
||||
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||
import { GetItems } from '../GetItems/GetItems'
|
||||
import { SaveItems } from '../SaveItems/SaveItems'
|
||||
|
||||
export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
constructor(private itemService: ItemServiceInterface) {}
|
||||
constructor(
|
||||
private itemRepository: ItemRepositoryInterface,
|
||||
private getItemsUseCase: GetItems,
|
||||
private saveItemsUseCase: SaveItems,
|
||||
) {}
|
||||
|
||||
async execute(dto: SyncItemsDTO): Promise<Result<SyncItemsResponse>> {
|
||||
const getItemsResult = await this.itemService.getItems({
|
||||
const getItemsResultOrError = await this.getItemsUseCase.execute({
|
||||
userUuid: dto.userUuid,
|
||||
syncToken: dto.syncToken,
|
||||
cursorToken: dto.cursorToken,
|
||||
limit: dto.limit,
|
||||
contentType: dto.contentType,
|
||||
})
|
||||
if (getItemsResultOrError.isFailed()) {
|
||||
return Result.fail(getItemsResultOrError.getError())
|
||||
}
|
||||
const getItemsResult = getItemsResultOrError.getValue()
|
||||
|
||||
const saveItemsResult = await this.itemService.saveItems({
|
||||
const saveItemsResultOrError = await this.saveItemsUseCase.execute({
|
||||
itemHashes: dto.itemHashes,
|
||||
userUuid: dto.userUuid,
|
||||
apiVersion: dto.apiVersion,
|
||||
readOnlyAccess: dto.readOnlyAccess,
|
||||
sessionUuid: dto.sessionUuid,
|
||||
})
|
||||
if (saveItemsResultOrError.isFailed()) {
|
||||
return Result.fail(saveItemsResultOrError.getError())
|
||||
}
|
||||
const saveItemsResult = saveItemsResultOrError.getValue()
|
||||
|
||||
let retrievedItems = this.filterOutSyncConflictsForConsecutiveSyncs(getItemsResult.items, saveItemsResult.conflicts)
|
||||
if (this.isFirstSync(dto)) {
|
||||
retrievedItems = await this.itemService.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems)
|
||||
retrievedItems = await this.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems)
|
||||
}
|
||||
|
||||
const syncResponse: SyncItemsResponse = {
|
||||
@@ -59,4 +73,23 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
|
||||
|
||||
return retrievedItems.filter((item: Item) => syncConflictIds.indexOf(item.id.toString()) === -1)
|
||||
}
|
||||
|
||||
private async frontLoadKeysItemsToTop(userUuid: string, retrievedItems: Array<Item>): Promise<Array<Item>> {
|
||||
const itemsKeys = await this.itemRepository.findAll({
|
||||
userUuid,
|
||||
contentType: ContentType.TYPES.ItemsKey,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
})
|
||||
|
||||
const retrievedItemsIds: Array<string> = retrievedItems.map((item: Item) => item.id.toString())
|
||||
|
||||
itemsKeys.forEach((itemKey: Item) => {
|
||||
if (retrievedItemsIds.indexOf(itemKey.id.toString()) === -1) {
|
||||
retrievedItems.unshift(itemKey)
|
||||
}
|
||||
})
|
||||
|
||||
return retrievedItems
|
||||
}
|
||||
}
|
||||
|
||||
+29
-26
@@ -37,8 +37,11 @@ describe('UpdateExistingItem', () => {
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemHash1 = {
|
||||
itemHash1 = ItemHash.create({
|
||||
uuid: '1-2-3',
|
||||
user_uuid: '00000000-0000-0000-0000-000000000000',
|
||||
key_system_identifier: null,
|
||||
shared_vault_uuid: null,
|
||||
content: 'asdqwe1',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
duplicate_of: null,
|
||||
@@ -53,7 +56,7 @@ describe('UpdateExistingItem', () => {
|
||||
new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
|
||||
),
|
||||
} as jest.Mocked<ItemHash>
|
||||
}).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.save = jest.fn()
|
||||
@@ -107,10 +110,10 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
content_type: 'invalid',
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
@@ -122,10 +125,10 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
deleted: true,
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
@@ -144,10 +147,10 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
duplicate_of: '00000000-0000-0000-0000-000000000001',
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
@@ -161,10 +164,10 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
duplicate_of: 'invalid-uuid',
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
@@ -176,11 +179,11 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
updated_at_timestamp: 123,
|
||||
created_at_timestamp: 123,
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
@@ -193,11 +196,11 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
created_at: undefined,
|
||||
created_at_timestamp: undefined,
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
@@ -214,11 +217,11 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
@@ -237,11 +240,11 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
existingItem: item1,
|
||||
itemHash: {
|
||||
...itemHash1,
|
||||
itemHash: ItemHash.create({
|
||||
...itemHash1.props,
|
||||
created_at_timestamp: 123,
|
||||
updated_at_timestamp: 123,
|
||||
},
|
||||
}).getValue(),
|
||||
sessionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
|
||||
+20
-20
@@ -27,12 +27,12 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
}
|
||||
dto.existingItem.props.updatedWithSession = sessionUuid
|
||||
|
||||
if (dto.itemHash.content) {
|
||||
dto.existingItem.props.content = dto.itemHash.content
|
||||
if (dto.itemHash.props.content) {
|
||||
dto.existingItem.props.content = dto.itemHash.props.content
|
||||
}
|
||||
|
||||
if (dto.itemHash.content_type) {
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.content_type)
|
||||
if (dto.itemHash.props.content_type) {
|
||||
const contentTypeOrError = ContentType.create(dto.itemHash.props.content_type)
|
||||
if (contentTypeOrError.isFailed()) {
|
||||
return Result.fail(contentTypeOrError.getError())
|
||||
}
|
||||
@@ -40,13 +40,13 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
dto.existingItem.props.contentType = contentType
|
||||
}
|
||||
|
||||
if (dto.itemHash.deleted !== undefined) {
|
||||
dto.existingItem.props.deleted = dto.itemHash.deleted
|
||||
if (dto.itemHash.props.deleted !== undefined) {
|
||||
dto.existingItem.props.deleted = dto.itemHash.props.deleted
|
||||
}
|
||||
|
||||
let wasMarkedAsDuplicate = false
|
||||
if (dto.itemHash.duplicate_of) {
|
||||
const duplicateOfOrError = Uuid.create(dto.itemHash.duplicate_of)
|
||||
if (dto.itemHash.props.duplicate_of) {
|
||||
const duplicateOfOrError = Uuid.create(dto.itemHash.props.duplicate_of)
|
||||
if (duplicateOfOrError.isFailed()) {
|
||||
return Result.fail(duplicateOfOrError.getError())
|
||||
}
|
||||
@@ -54,14 +54,14 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
dto.existingItem.props.duplicateOf = duplicateOfOrError.getValue()
|
||||
}
|
||||
|
||||
if (dto.itemHash.auth_hash) {
|
||||
dto.existingItem.props.authHash = dto.itemHash.auth_hash
|
||||
if (dto.itemHash.props.auth_hash) {
|
||||
dto.existingItem.props.authHash = dto.itemHash.props.auth_hash
|
||||
}
|
||||
if (dto.itemHash.enc_item_key) {
|
||||
dto.existingItem.props.encItemKey = dto.itemHash.enc_item_key
|
||||
if (dto.itemHash.props.enc_item_key) {
|
||||
dto.existingItem.props.encItemKey = dto.itemHash.props.enc_item_key
|
||||
}
|
||||
if (dto.itemHash.items_key_id) {
|
||||
dto.existingItem.props.itemsKeyId = dto.itemHash.items_key_id
|
||||
if (dto.itemHash.props.items_key_id) {
|
||||
dto.existingItem.props.itemsKeyId = dto.itemHash.props.items_key_id
|
||||
}
|
||||
|
||||
const updatedAtTimestamp = this.timer.getTimestampInMicroseconds()
|
||||
@@ -72,12 +72,12 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
|
||||
let createdAtTimestamp: number
|
||||
let createdAtDate: Date
|
||||
if (dto.itemHash.created_at_timestamp) {
|
||||
createdAtTimestamp = dto.itemHash.created_at_timestamp
|
||||
if (dto.itemHash.props.created_at_timestamp) {
|
||||
createdAtTimestamp = dto.itemHash.props.created_at_timestamp
|
||||
createdAtDate = this.timer.convertMicrosecondsToDate(createdAtTimestamp)
|
||||
} else if (dto.itemHash.created_at) {
|
||||
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.created_at)
|
||||
createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.created_at)
|
||||
} else if (dto.itemHash.props.created_at) {
|
||||
createdAtTimestamp = this.timer.convertStringDateToMicroseconds(dto.itemHash.props.created_at)
|
||||
createdAtDate = this.timer.convertStringDateToDate(dto.itemHash.props.created_at)
|
||||
} else {
|
||||
return Result.fail('Created at timestamp is required.')
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
|
||||
dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
|
||||
|
||||
if (dto.itemHash.deleted === true) {
|
||||
if (dto.itemHash.props.deleted === true) {
|
||||
dto.existingItem.props.deleted = true
|
||||
dto.existingItem.props.content = null
|
||||
dto.existingItem.props.contentSize = 0
|
||||
|
||||
+16
-2
@@ -10,6 +10,7 @@ import { ApiVersion } from '../../../Domain/Api/ApiVersion'
|
||||
import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { HttpStatusCode } from '@standardnotes/responses'
|
||||
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
|
||||
import { ItemHash } from '../../../Domain/Item/ItemHash'
|
||||
|
||||
export class HomeServerItemsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -30,9 +31,22 @@ export class HomeServerItemsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async sync(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
let itemHashes = []
|
||||
const itemHashes: ItemHash[] = []
|
||||
if ('items' in request.body) {
|
||||
itemHashes = request.body.items
|
||||
for (const itemHashInput of request.body.items) {
|
||||
const itemHashOrError = ItemHash.create({
|
||||
...itemHashInput,
|
||||
user_uuid: response.locals.user.uuid,
|
||||
key_system_identifier: itemHashInput.key_system_identifier ?? null,
|
||||
shared_vault_uuid: itemHashInput.shared_vault_uuid ?? null,
|
||||
})
|
||||
|
||||
if (itemHashOrError.isFailed()) {
|
||||
return this.json({ error: { message: itemHashOrError.getError() } }, HttpStatusCode.BadRequest)
|
||||
}
|
||||
|
||||
itemHashes.push(itemHashOrError.getValue())
|
||||
}
|
||||
}
|
||||
|
||||
const syncResult = await this.syncItems.execute({
|
||||
|
||||
-238
@@ -1,238 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as express from 'express'
|
||||
import { ContentType, MapperInterface, Result } from '@standardnotes/domain-core'
|
||||
import { results } from 'inversify-express-utils'
|
||||
|
||||
import { InversifyExpressItemsController } from './InversifyExpressItemsController'
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { ApiVersion } from '../../Domain/Api/ApiVersion'
|
||||
import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115'
|
||||
import { SyncResponseFactoryInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryInterface'
|
||||
import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
|
||||
import { CheckIntegrity } from '../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
|
||||
import { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem'
|
||||
import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
|
||||
|
||||
describe('InversifyExpressItemsController', () => {
|
||||
let syncItems: SyncItems
|
||||
let checkIntegrity: CheckIntegrity
|
||||
let getItem: GetItem
|
||||
let mapper: MapperInterface<Item, ItemHttpRepresentation>
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface
|
||||
let syncResponseFactory: SyncResponseFactoryInterface
|
||||
let syncResponse: SyncResponse20200115
|
||||
|
||||
const createController = () =>
|
||||
new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, mapper, syncResponceFactoryResolver)
|
||||
|
||||
beforeEach(() => {
|
||||
mapper = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
|
||||
mapper.toProjection = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
syncItems = {} as jest.Mocked<SyncItems>
|
||||
syncItems.execute = jest.fn().mockReturnValue(Result.ok({ foo: 'bar' }))
|
||||
|
||||
checkIntegrity = {} as jest.Mocked<CheckIntegrity>
|
||||
checkIntegrity.execute = jest.fn().mockReturnValue(Result.ok([{ uuid: '1-2-3', updated_at_timestamp: 2 }]))
|
||||
|
||||
getItem = {} as jest.Mocked<GetItem>
|
||||
getItem.execute = jest.fn().mockReturnValue(Result.ok({} as jest.Mocked<Item>))
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
body: {},
|
||||
params: {},
|
||||
} as jest.Mocked<express.Request>
|
||||
|
||||
request.body.api = ApiVersion.v20200115
|
||||
request.body.sync_token = 'MjoxNjE3MTk1MzQyLjc1ODEyMTc='
|
||||
request.body.limit = 150
|
||||
request.body.compute_integrity = false
|
||||
request.headers['user-agent'] = 'Google Chrome'
|
||||
request.body.items = [
|
||||
{
|
||||
content: 'test',
|
||||
content_type: ContentType.TYPES.Note,
|
||||
created_at: '2021-02-19T11:35:45.655Z',
|
||||
deleted: false,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'test',
|
||||
items_key_id: 'test',
|
||||
updated_at: '2021-02-19T11:35:45.655Z',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
]
|
||||
|
||||
response = {
|
||||
locals: {},
|
||||
} as jest.Mocked<express.Response>
|
||||
response.locals.user = {
|
||||
uuid: '123',
|
||||
}
|
||||
response.locals.freeUser = false
|
||||
|
||||
syncResponse = {} as jest.Mocked<SyncResponse20200115>
|
||||
|
||||
syncResponseFactory = {} as jest.Mocked<SyncResponseFactoryInterface>
|
||||
syncResponseFactory.createResponse = jest.fn().mockReturnValue(syncResponse)
|
||||
|
||||
syncResponceFactoryResolver = {} as jest.Mocked<SyncResponseFactoryResolverInterface>
|
||||
syncResponceFactoryResolver.resolveSyncResponseFactoryVersion = jest.fn().mockReturnValue(syncResponseFactory)
|
||||
})
|
||||
|
||||
it('should get a single item', async () => {
|
||||
request.params.uuid = '1-2-3'
|
||||
const httpResponse = <results.JsonResult>await createController().getSingleItem(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(getItem.execute).toHaveBeenCalledWith({
|
||||
itemUuid: '1-2-3',
|
||||
userUuid: '123',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should return 404 on a missing single item', async () => {
|
||||
request.params.uuid = '1-2-3'
|
||||
getItem.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.NotFoundResult>await createController().getSingleItem(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(getItem.execute).toHaveBeenCalledWith({
|
||||
itemUuid: '1-2-3',
|
||||
userUuid: '123',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(404)
|
||||
})
|
||||
|
||||
it('should check items integrity', async () => {
|
||||
request.body.integrityPayloads = [
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().checkItemsIntegrity(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(checkIntegrity.execute).toHaveBeenCalledWith({
|
||||
integrityPayloads: [
|
||||
{
|
||||
updated_at_timestamp: 1,
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
],
|
||||
userUuid: '123',
|
||||
freeUser: false,
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(await result.content.readAsStringAsync()).toEqual(
|
||||
'{"mismatches":[{"uuid":"1-2-3","updated_at_timestamp":2}]}',
|
||||
)
|
||||
})
|
||||
|
||||
it('should check items integrity with missing request parameter', async () => {
|
||||
const httpResponse = <results.JsonResult>await createController().checkItemsIntegrity(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(checkIntegrity.execute).toHaveBeenCalledWith({
|
||||
integrityPayloads: [],
|
||||
userUuid: '123',
|
||||
freeUser: false,
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
expect(await result.content.readAsStringAsync()).toEqual(
|
||||
'{"mismatches":[{"uuid":"1-2-3","updated_at_timestamp":2}]}',
|
||||
)
|
||||
})
|
||||
|
||||
it('should sync items', async () => {
|
||||
const httpResponse = <results.JsonResult>await createController().sync(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(syncItems.execute).toHaveBeenCalledWith({
|
||||
apiVersion: '20200115',
|
||||
computeIntegrityHash: false,
|
||||
itemHashes: [
|
||||
{
|
||||
content: 'test',
|
||||
content_type: 'Note',
|
||||
created_at: '2021-02-19T11:35:45.655Z',
|
||||
deleted: false,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'test',
|
||||
items_key_id: 'test',
|
||||
updated_at: '2021-02-19T11:35:45.655Z',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
],
|
||||
limit: 150,
|
||||
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
|
||||
userUuid: '123',
|
||||
sessionUuid: null,
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should sync items with defaulting API version if none specified', async () => {
|
||||
delete request.body.api
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().sync(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(syncItems.execute).toHaveBeenCalledWith({
|
||||
apiVersion: '20161215',
|
||||
computeIntegrityHash: false,
|
||||
itemHashes: [
|
||||
{
|
||||
content: 'test',
|
||||
content_type: 'Note',
|
||||
created_at: '2021-02-19T11:35:45.655Z',
|
||||
deleted: false,
|
||||
duplicate_of: null,
|
||||
enc_item_key: 'test',
|
||||
items_key_id: 'test',
|
||||
updated_at: '2021-02-19T11:35:45.655Z',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
],
|
||||
limit: 150,
|
||||
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
|
||||
userUuid: '123',
|
||||
sessionUuid: null,
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should sync items with no incoming items in request', async () => {
|
||||
response.locals.session = { uuid: '2-3-4' }
|
||||
delete request.body.items
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().sync(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(syncItems.execute).toHaveBeenCalledWith({
|
||||
apiVersion: '20200115',
|
||||
computeIntegrityHash: false,
|
||||
itemHashes: [],
|
||||
limit: 150,
|
||||
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
|
||||
userUuid: '123',
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'shared_vault_items' })
|
||||
export class TypeORMSharedVaultItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
declare uuid: string
|
||||
|
||||
@Column({
|
||||
name: 'shared_vault_uuid',
|
||||
length: 36,
|
||||
})
|
||||
declare sharedVaultUuid: string
|
||||
|
||||
@Column({
|
||||
name: 'item_uuid',
|
||||
length: 36,
|
||||
})
|
||||
declare itemUuid: string
|
||||
|
||||
@Column({
|
||||
name: 'key_system_identifier',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
})
|
||||
declare keySystemIdentifier: string
|
||||
|
||||
@Column({
|
||||
name: 'last_edited_by',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
})
|
||||
declare lastEditedBy: string
|
||||
|
||||
@Column({
|
||||
name: 'created_at_timestamp',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare createdAtTimestamp: number
|
||||
|
||||
@Column({
|
||||
name: 'updated_at_timestamp',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare updatedAtTimestamp: number
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Repository } from 'typeorm'
|
||||
import { MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultItem } from '../../Domain/SharedVault/Item/SharedVaultItem'
|
||||
import { SharedVaultItemRepositoryInterface } from '../../Domain/SharedVault/Item/SharedVaultItemRepositoryInterface'
|
||||
import { TypeORMSharedVaultItem } from './TypeORMSharedVaultItem'
|
||||
|
||||
export class TypeORMSharedVaultItemRepository implements SharedVaultItemRepositoryInterface {
|
||||
constructor(
|
||||
private ormRepository: Repository<TypeORMSharedVaultItem>,
|
||||
private mapper: MapperInterface<SharedVaultItem, TypeORMSharedVaultItem>,
|
||||
) {}
|
||||
|
||||
async findBySharedVaultId(sharedVaultId: UniqueEntityId): Promise<SharedVaultItem[]> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('shared_vault_item')
|
||||
.where('shared_vault_item.shared_vault_uuid = :sharedVaultUuid', {
|
||||
sharedVaultUuid: sharedVaultId.toString(),
|
||||
})
|
||||
.getMany()
|
||||
|
||||
return persistence.map((p) => this.mapper.toDomain(p))
|
||||
}
|
||||
|
||||
async save(sharedVaultItem: SharedVaultItem): Promise<void> {
|
||||
const persistence = this.mapper.toProjection(sharedVaultItem)
|
||||
|
||||
await this.ormRepository.save(persistence)
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,17 @@ import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { SharedVaultRepositoryInterface } from '../../Domain/SharedVault/SharedVaultRepositoryInterface'
|
||||
import { TypeORMSharedVault } from './TypeORMSharedVault'
|
||||
import { SharedVault } from '../../Domain/SharedVault/SharedVault'
|
||||
import { SharedVaultItemRepositoryInterface } from '../../Domain/SharedVault/Item/SharedVaultItemRepositoryInterface'
|
||||
|
||||
export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterface {
|
||||
constructor(
|
||||
private ormRepository: Repository<TypeORMSharedVault>,
|
||||
private sharedVaultItemRepository: SharedVaultItemRepositoryInterface,
|
||||
private mapper: MapperInterface<SharedVault, TypeORMSharedVault>,
|
||||
) {}
|
||||
|
||||
async findByUuids(uuids: Uuid[], lastSyncTime?: number | undefined): Promise<SharedVault[]> {
|
||||
const queryBuilder = await this.ormRepository
|
||||
const queryBuilder = this.ormRepository
|
||||
.createQueryBuilder('shared_vault')
|
||||
.where('shared_vault.uuid IN (:...sharedVaultUuids)', { sharedVaultUuids: uuids.map((uuid) => uuid.value) })
|
||||
|
||||
@@ -26,6 +28,10 @@ export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterf
|
||||
}
|
||||
|
||||
async save(sharedVault: SharedVault): Promise<void> {
|
||||
for (const item of sharedVault.props.sharedVaultItems) {
|
||||
await this.sharedVaultItemRepository.save(item)
|
||||
}
|
||||
|
||||
const persistence = this.mapper.toProjection(sharedVault)
|
||||
|
||||
await this.ormRepository.save(persistence)
|
||||
|
||||
@@ -4,9 +4,14 @@ import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemConflictHttpRepresentation } from './ItemConflictHttpRepresentation'
|
||||
import { ItemConflict } from '../../Domain/Item/ItemConflict'
|
||||
import { ItemHttpRepresentation } from './ItemHttpRepresentation'
|
||||
import { ItemHash } from '../../Domain/Item/ItemHash'
|
||||
import { ItemHashHttpRepresentation } from './ItemHashHttpRepresentation'
|
||||
|
||||
export class ItemConflictHttpMapper implements MapperInterface<ItemConflict, ItemConflictHttpRepresentation> {
|
||||
constructor(private mapper: MapperInterface<Item, ItemHttpRepresentation>) {}
|
||||
constructor(
|
||||
private mapper: MapperInterface<Item, ItemHttpRepresentation>,
|
||||
private itemHashMapper: MapperInterface<ItemHash, ItemHashHttpRepresentation>,
|
||||
) {}
|
||||
|
||||
toDomain(_projection: ItemConflictHttpRepresentation): ItemConflict {
|
||||
throw new Error('Mapping from http representation to domain is not implemented.')
|
||||
@@ -14,10 +19,13 @@ export class ItemConflictHttpMapper implements MapperInterface<ItemConflict, Ite
|
||||
|
||||
toProjection(domain: ItemConflict): ItemConflictHttpRepresentation {
|
||||
const representation: ItemConflictHttpRepresentation = {
|
||||
unsaved_item: domain.unsavedItem,
|
||||
type: domain.type,
|
||||
}
|
||||
|
||||
if (domain.unsavedItem) {
|
||||
representation.unsaved_item = this.itemHashMapper.toProjection(domain.unsavedItem)
|
||||
}
|
||||
|
||||
if (domain.serverItem) {
|
||||
representation.server_item = this.mapper.toProjection(domain.serverItem)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ConflictType } from '@standardnotes/responses'
|
||||
|
||||
import { ItemHash } from '../../Domain/Item/ItemHash'
|
||||
import { ItemHttpRepresentation } from './ItemHttpRepresentation'
|
||||
import { ItemHashHttpRepresentation } from './ItemHashHttpRepresentation'
|
||||
|
||||
export interface ItemConflictHttpRepresentation {
|
||||
server_item?: ItemHttpRepresentation
|
||||
unsaved_item?: ItemHash
|
||||
unsaved_item?: ItemHashHttpRepresentation
|
||||
type: ConflictType
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemHashHttpRepresentation } from './ItemHashHttpRepresentation'
|
||||
import { ItemHash } from '../../Domain/Item/ItemHash'
|
||||
|
||||
export class ItemHashHttpMapper implements MapperInterface<ItemHash, ItemHashHttpRepresentation> {
|
||||
toDomain(_projection: ItemHashHttpRepresentation): ItemHash {
|
||||
throw new Error('Mapping from http representation to domain is not implemented.')
|
||||
}
|
||||
|
||||
toProjection(domain: ItemHash): ItemHashHttpRepresentation {
|
||||
return {
|
||||
...domain.props,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface ItemHashHttpRepresentation {
|
||||
uuid: string
|
||||
user_uuid: string
|
||||
content?: string
|
||||
content_type: string | null
|
||||
deleted?: boolean
|
||||
duplicate_of?: string | null
|
||||
auth_hash?: string
|
||||
enc_item_key?: string
|
||||
items_key_id?: string
|
||||
key_system_identifier: string | null
|
||||
shared_vault_uuid: string | null
|
||||
created_at?: string
|
||||
created_at_timestamp?: number
|
||||
updated_at?: string
|
||||
updated_at_timestamp?: number
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export class SharedVaultPersistenceMapper implements MapperInterface<SharedVault
|
||||
fileUploadBytesUsed: projection.fileUploadBytesUsed,
|
||||
fileUploadBytesLimit: projection.fileUploadBytesLimit,
|
||||
timestamps,
|
||||
sharedVaultItems: [],
|
||||
},
|
||||
new UniqueEntityId(projection.uuid),
|
||||
)
|
||||
|
||||
@@ -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.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.1...@standardnotes/websockets-server@1.10.2) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.0...@standardnotes/websockets-server@1.10.1) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.10.1",
|
||||
"version": "1.10.2",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user