From b614c71e79959a24379567a3b5cb3a3e66fde6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 5 Jul 2022 20:47:11 +0200 Subject: [PATCH] feat: add models package --- .gitignore | 1 + ...dels-npm-1.11.12-d07d5ebeb6-636897db97.zip | Bin 232094 -> 0 bytes ...dels-npm-1.11.13-8272aa4de5-063f4382b8.zip | Bin 232109 -> 0 bytes packages/encryption/package.json | 2 +- packages/files/package.json | 2 +- packages/models/.eslintignore | 2 + packages/models/.eslintrc | 10 + packages/models/CHANGELOG.md | 314 +++++++++ packages/models/jest.config.js | 11 + packages/models/linter.tsconfig.json | 4 + packages/models/package.json | 43 ++ .../Domain/Abstract/Content/ItemContent.ts | 51 ++ .../Domain/Abstract/Contextual/BackupFile.ts | 60 ++ .../Abstract/Contextual/ComponentCreate.ts | 24 + .../Abstract/Contextual/ComponentRetrieved.ts | 24 + .../Abstract/Contextual/ContextPayload.ts | 9 + .../Abstract/Contextual/FilteredServerItem.ts | 25 + .../Abstract/Contextual/LocalStorage.ts | 107 +++ .../Abstract/Contextual/OfflineSyncPush.ts | 41 ++ .../Abstract/Contextual/OfflineSyncSaved.ts | 30 + .../Abstract/Contextual/ServerSyncPush.ts | 50 ++ .../Abstract/Contextual/ServerSyncSaved.ts | 31 + .../Abstract/Contextual/SessionHistory.ts | 7 + .../src/Domain/Abstract/Contextual/index.ts | 10 + .../Item/Implementations/DecryptedItem.ts | 122 ++++ .../Item/Implementations/DeletedItem.ts | 18 + .../Item/Implementations/EncryptedItem.ts | 34 + .../Item/Implementations/GenericItem.ts | 189 ++++++ .../Abstract/Item/Interfaces/DecryptedItem.ts | 45 ++ .../Abstract/Item/Interfaces/DeletedItem.ts | 7 + .../Abstract/Item/Interfaces/EncryptedItem.ts | 11 + .../Abstract/Item/Interfaces/ItemInterface.ts | 41 ++ .../Abstract/Item/Interfaces/TypeCheck.ts | 31 + .../Abstract/Item/Interfaces/UnionTypes.ts | 9 + .../Item/Mutator/DecryptedItemMutator.ts | 145 ++++ .../Abstract/Item/Mutator/DeleteMutator.ts | 30 + .../Abstract/Item/Mutator/ItemMutator.ts | 65 ++ .../Abstract/Item/Types/AppDataField.ts | 13 + .../Abstract/Item/Types/ConflictStrategy.ts | 7 + .../Abstract/Item/Types/DefaultAppDomain.ts | 17 + .../Abstract/Item/Types/MutationType.ts | 14 + .../Abstract/Item/Types/SingletonStrategy.ts | 3 + .../models/src/Domain/Abstract/Item/index.ts | 29 + .../Implementations/DecryptedPayload.ts | 71 ++ .../Payload/Implementations/DeletedPayload.ts | 54 ++ .../Implementations/EncryptedPayload.ts | 68 ++ .../Payload/Implementations/PurePayload.ts | 104 +++ .../Payload/Interfaces/DecryptedPayload.ts | 15 + .../Payload/Interfaces/DeletedPayload.ts | 15 + .../Payload/Interfaces/EncryptedPayload.ts | 18 + .../Payload/Interfaces/PayloadInterface.ts | 41 ++ .../Abstract/Payload/Interfaces/TypeCheck.ts | 29 + .../Abstract/Payload/Interfaces/UnionTypes.ts | 11 + .../Abstract/Payload/Types/EmitSource.ts | 43 ++ .../Abstract/Payload/Types/PayloadSource.ts | 13 + .../Payload/Types/TimestampDefaults.ts | 8 + .../src/Domain/Abstract/Payload/index.ts | 13 + .../Abstract/Reference/AnonymousReference.ts | 8 + .../Abstract/Reference/ContentReference.ts | 4 + .../Reference/ContenteReferenceType.ts | 5 + .../Abstract/Reference/FileToNoteReference.ts | 8 + .../Domain/Abstract/Reference/Functions.ts | 30 + .../Reference/LegacyAnonymousReference.ts | 4 + .../Reference/LegacyTagToNoteReference.ts | 6 + .../Domain/Abstract/Reference/Reference.ts | 3 + .../Abstract/Reference/TagToFileReference.ts | 8 + .../Reference/TagToParentTagReference.ts | 8 + .../Interfaces/DecryptedTransferPayload.ts | 6 + .../Interfaces/DeletedTransferPayload.ts | 6 + .../Interfaces/EncryptedTransferPayload.ts | 11 + .../Interfaces/TransferPayload.ts | 23 + .../TransferPayload/Interfaces/TypeCheck.ts | 28 + .../Domain/Abstract/TransferPayload/index.ts | 5 + .../Local/KeyParams/RootKeyParamsInterface.ts | 47 ++ .../src/Domain/Local/RootKey/KeychainTypes.ts | 31 + .../Domain/Local/RootKey/RootKeyContent.ts | 12 + .../Domain/Local/RootKey/RootKeyInterface.ts | 17 + .../Domain/Runtime/Collection/Collection.ts | 263 +++++++ .../Runtime/Collection/CollectionInterface.ts | 13 + .../Runtime/Collection/CollectionSort.ts | 20 + .../Collection/Item/ItemCollection.spec.ts | 36 + .../Runtime/Collection/Item/ItemCollection.ts | 59 ++ .../Collection/Item/TagNotesIndex.spec.ts | 65 ++ .../Runtime/Collection/Item/TagNotesIndex.ts | 111 +++ .../Payload/ImmutablePayloadCollection.ts | 54 ++ .../Collection/Payload/PayloadCollection.ts | 21 + .../Runtime/Deltas/Abstract/DeltaEmit.ts | 30 + .../Runtime/Deltas/Abstract/DeltaInterface.ts | 24 + .../Deltas/Abstract/SyncDeltaInterface.ts | 8 + .../Domain/Runtime/Deltas/Conflict.spec.ts | 102 +++ .../src/Domain/Runtime/Deltas/Conflict.ts | 225 ++++++ .../src/Domain/Runtime/Deltas/FileImport.ts | 90 +++ .../Runtime/Deltas/ItemsKeyDelta.spec.ts | 54 ++ .../Domain/Runtime/Deltas/ItemsKeyDelta.ts | 52 ++ .../src/Domain/Runtime/Deltas/OfflineSaved.ts | 33 + .../src/Domain/Runtime/Deltas/OutOfSync.ts | 62 ++ .../Runtime/Deltas/RemoteDataConflicts.ts | 41 ++ .../Runtime/Deltas/RemoteRejected.spec.ts | 45 ++ .../Domain/Runtime/Deltas/RemoteRejected.ts | 40 ++ .../Runtime/Deltas/RemoteRetrieved.spec.ts | 60 ++ .../Domain/Runtime/Deltas/RemoteRetrieved.ts | 87 +++ .../src/Domain/Runtime/Deltas/RemoteSaved.ts | 99 +++ .../Runtime/Deltas/RemoteUuidConflicts.ts | 56 ++ .../Deltas/Utilities/ApplyDirtyState.ts | 36 + .../Deltas/Utilities/SyncResolvedPayload.ts | 12 + .../models/src/Domain/Runtime/Deltas/index.ts | 10 + .../Runtime/DirtyCounter/DirtyCounter.ts | 10 + .../Runtime/Display/DisplayOptions.spec.ts | 53 ++ .../Domain/Runtime/Display/DisplayOptions.ts | 25 + .../Display/DisplayOptionsToFilters.ts | 78 +++ .../Display/ItemDisplayController.spec.ts | 256 +++++++ .../Runtime/Display/ItemDisplayController.ts | 138 ++++ .../Runtime/Display/Search/ItemWithTags.ts | 36 + .../Runtime/Display/Search/SearchUtilities.ts | 97 +++ .../Runtime/Display/Search/SearchableItem.ts | 5 + .../Domain/Runtime/Display/Search/Types.ts | 16 + .../Runtime/Display/SortTwoItems.spec.ts | 29 + .../Domain/Runtime/Display/SortTwoItems.ts | 83 +++ .../src/Domain/Runtime/Display/Types.ts | 13 + .../src/Domain/Runtime/Display/index.ts | 8 + .../src/Domain/Runtime/History/Generator.ts | 24 + .../Domain/Runtime/History/HistoryEntry.ts | 98 +++ .../Runtime/History/HistoryEntryInterface.ts | 13 + .../src/Domain/Runtime/History/HistoryMap.ts | 10 + .../Runtime/History/NoteHistoryEntry.ts | 28 + .../src/Domain/Runtime/History/index.ts | 5 + .../src/Domain/Runtime/Index/ItemDelta.ts | 24 + .../src/Domain/Runtime/Index/SNIndex.ts | 5 + .../Runtime/Predicate/CompoundPredicate.ts | 46 ++ .../Domain/Runtime/Predicate/Generators.ts | 140 ++++ .../Runtime/Predicate/IncludesPredicate.ts | 32 + .../src/Domain/Runtime/Predicate/Interface.ts | 45 ++ .../Domain/Runtime/Predicate/NotPredicate.ts | 20 + .../src/Domain/Runtime/Predicate/Operator.ts | 95 +++ .../Runtime/Predicate/Predicate.spec.ts | 639 ++++++++++++++++++ .../src/Domain/Runtime/Predicate/Predicate.ts | 49 ++ .../src/Domain/Runtime/Predicate/Utils.ts | 15 + .../ActionsExtension/ActionsExtension.ts | 72 ++ .../ActionsExtensionMutator.ts | 21 + .../Domain/Syncable/ActionsExtension/Types.ts | 25 + .../Domain/Syncable/ActionsExtension/index.ts | 3 + .../Syncable/Component/Component.spec.ts | 49 ++ .../Domain/Syncable/Component/Component.ts | 189 ++++++ .../Syncable/Component/ComponentContent.ts | 36 + .../Syncable/Component/ComponentMutator.ts | 76 +++ .../Domain/Syncable/Component/PackageInfo.ts | 8 + .../src/Domain/Syncable/Component/index.ts | 3 + .../src/Domain/Syncable/Editor/Editor.ts | 35 + .../src/Domain/Syncable/Editor/index.ts | 1 + .../Syncable/FeatureRepo/FeatureRepo.ts | 33 + .../FeatureRepo/FeatureRepoMutator.ts | 20 + .../src/Domain/Syncable/FeatureRepo/index.ts | 2 + .../src/Domain/Syncable/File/File.spec.ts | 75 ++ .../models/src/Domain/Syncable/File/File.ts | 85 +++ .../src/Domain/Syncable/File/FileMetadata.ts | 4 + .../src/Domain/Syncable/File/FileMutator.ts | 33 + .../Domain/Syncable/File/FileProtocolV1.ts | 9 + .../models/src/Domain/Syncable/File/index.ts | 4 + .../Syncable/ItemsKey/ItemsKeyInterface.ts | 19 + .../ItemsKey/ItemsKeyMutatorInterface.ts | 5 + .../src/Domain/Syncable/Note/Note.spec.ts | 42 ++ .../models/src/Domain/Syncable/Note/Note.ts | 34 + .../src/Domain/Syncable/Note/NoteContent.ts | 13 + .../src/Domain/Syncable/Note/NoteMutator.ts | 41 ++ .../models/src/Domain/Syncable/Note/index.ts | 3 + .../Domain/Syncable/SmartView/SmartView.ts | 44 ++ .../Syncable/SmartView/SmartViewBuilder.ts | 179 +++++ .../src/Domain/Syncable/SmartView/index.ts | 2 + .../src/Domain/Syncable/Tag/Tag.spec.ts | 40 ++ .../models/src/Domain/Syncable/Tag/Tag.ts | 56 ++ .../Domain/Syncable/Tag/TagMutator.spec.ts | 38 ++ .../src/Domain/Syncable/Tag/TagMutator.ts | 70 ++ .../models/src/Domain/Syncable/Tag/index.ts | 2 + .../models/src/Domain/Syncable/Theme/Theme.ts | 48 ++ .../src/Domain/Syncable/Theme/ThemeMutator.ts | 25 + .../models/src/Domain/Syncable/Theme/index.ts | 2 + .../src/Domain/Syncable/UserPrefs/PrefKey.ts | 68 ++ .../Domain/Syncable/UserPrefs/UserPrefs.ts | 20 + .../Syncable/UserPrefs/UserPrefsMutator.ts | 8 + .../src/Domain/Syncable/UserPrefs/index.ts | 3 + .../src/Domain/Utilities/Item/FindItem.ts | 10 + .../Utilities/Item/ItemContentsDiffer.ts | 16 + .../Utilities/Item/ItemContentsEqual.ts | 47 ++ .../Domain/Utilities/Item/ItemGenerator.ts | 113 ++++ .../Utilities/Payload/AffectorFunction.ts | 55 ++ .../Payload/ConditionalPayloadType.ts | 20 + .../Payload/CopyPayloadWithContentOverride.ts | 19 + .../Domain/Utilities/Payload/CreatePayload.ts | 26 + .../Domain/Utilities/Payload/FindPayload.ts | 10 + .../Utilities/Payload/PayloadContentsEqual.ts | 15 + .../Domain/Utilities/Payload/PayloadSplit.ts | 98 +++ .../Payload/PayloadsByAlternatingUuid.ts | 97 +++ .../Payload/PayloadsByDuplicating.ts | 81 +++ ...sByUpdatingReferencingPayloadReferences.ts | 52 ++ .../src/Domain/Utilities/Test/SpecUtils.ts | 80 +++ packages/models/src/Domain/index.ts | 55 ++ packages/models/src/index.ts | 1 + packages/models/tsconfig.json | 13 + yarn.lock | 37 +- 199 files changed, 8772 insertions(+), 22 deletions(-) delete mode 100644 .yarn/cache/@standardnotes-models-npm-1.11.12-d07d5ebeb6-636897db97.zip delete mode 100644 .yarn/cache/@standardnotes-models-npm-1.11.13-8272aa4de5-063f4382b8.zip create mode 100644 packages/models/.eslintignore create mode 100644 packages/models/.eslintrc create mode 100644 packages/models/CHANGELOG.md create mode 100644 packages/models/jest.config.js create mode 100644 packages/models/linter.tsconfig.json create mode 100644 packages/models/package.json create mode 100644 packages/models/src/Domain/Abstract/Content/ItemContent.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/BackupFile.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/ComponentCreate.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/ComponentRetrieved.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/OfflineSyncPush.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/OfflineSyncSaved.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/ServerSyncPush.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/ServerSyncSaved.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/SessionHistory.ts create mode 100644 packages/models/src/Domain/Abstract/Contextual/index.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Implementations/DeletedItem.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Implementations/EncryptedItem.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Interfaces/DeletedItem.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Interfaces/EncryptedItem.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Interfaces/TypeCheck.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Interfaces/UnionTypes.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Mutator/DeleteMutator.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Types/ConflictStrategy.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Types/DefaultAppDomain.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Types/MutationType.ts create mode 100644 packages/models/src/Domain/Abstract/Item/Types/SingletonStrategy.ts create mode 100644 packages/models/src/Domain/Abstract/Item/index.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Implementations/DecryptedPayload.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Implementations/DeletedPayload.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Implementations/EncryptedPayload.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Interfaces/DecryptedPayload.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Interfaces/DeletedPayload.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Interfaces/TypeCheck.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Interfaces/UnionTypes.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Types/EmitSource.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/Types/TimestampDefaults.ts create mode 100644 packages/models/src/Domain/Abstract/Payload/index.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/AnonymousReference.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/ContentReference.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/ContenteReferenceType.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/FileToNoteReference.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/Functions.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/LegacyAnonymousReference.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/LegacyTagToNoteReference.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/Reference.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/TagToFileReference.ts create mode 100644 packages/models/src/Domain/Abstract/Reference/TagToParentTagReference.ts create mode 100644 packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DecryptedTransferPayload.ts create mode 100644 packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DeletedTransferPayload.ts create mode 100644 packages/models/src/Domain/Abstract/TransferPayload/Interfaces/EncryptedTransferPayload.ts create mode 100644 packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts create mode 100644 packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts create mode 100644 packages/models/src/Domain/Abstract/TransferPayload/index.ts create mode 100644 packages/models/src/Domain/Local/KeyParams/RootKeyParamsInterface.ts create mode 100644 packages/models/src/Domain/Local/RootKey/KeychainTypes.ts create mode 100644 packages/models/src/Domain/Local/RootKey/RootKeyContent.ts create mode 100644 packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/Collection.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/CollectionInterface.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/CollectionSort.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/Payload/ImmutablePayloadCollection.ts create mode 100644 packages/models/src/Domain/Runtime/Collection/Payload/PayloadCollection.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaEmit.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaInterface.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/Abstract/SyncDeltaInterface.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/Conflict.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/Conflict.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/FileImport.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/OfflineSaved.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/OutOfSync.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/Utilities/ApplyDirtyState.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/Utilities/SyncResolvedPayload.ts create mode 100644 packages/models/src/Domain/Runtime/Deltas/index.ts create mode 100644 packages/models/src/Domain/Runtime/DirtyCounter/DirtyCounter.ts create mode 100644 packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Display/DisplayOptions.ts create mode 100644 packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts create mode 100644 packages/models/src/Domain/Runtime/Display/ItemDisplayController.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Search/ItemWithTags.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Search/SearchableItem.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Search/Types.ts create mode 100644 packages/models/src/Domain/Runtime/Display/SortTwoItems.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Display/SortTwoItems.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Types.ts create mode 100644 packages/models/src/Domain/Runtime/Display/index.ts create mode 100644 packages/models/src/Domain/Runtime/History/Generator.ts create mode 100644 packages/models/src/Domain/Runtime/History/HistoryEntry.ts create mode 100644 packages/models/src/Domain/Runtime/History/HistoryEntryInterface.ts create mode 100644 packages/models/src/Domain/Runtime/History/HistoryMap.ts create mode 100644 packages/models/src/Domain/Runtime/History/NoteHistoryEntry.ts create mode 100644 packages/models/src/Domain/Runtime/History/index.ts create mode 100644 packages/models/src/Domain/Runtime/Index/ItemDelta.ts create mode 100644 packages/models/src/Domain/Runtime/Index/SNIndex.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/CompoundPredicate.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/Generators.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/IncludesPredicate.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/Interface.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/NotPredicate.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/Operator.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/Predicate.spec.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/Predicate.ts create mode 100644 packages/models/src/Domain/Runtime/Predicate/Utils.ts create mode 100644 packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtension.ts create mode 100644 packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtensionMutator.ts create mode 100644 packages/models/src/Domain/Syncable/ActionsExtension/Types.ts create mode 100644 packages/models/src/Domain/Syncable/ActionsExtension/index.ts create mode 100644 packages/models/src/Domain/Syncable/Component/Component.spec.ts create mode 100644 packages/models/src/Domain/Syncable/Component/Component.ts create mode 100644 packages/models/src/Domain/Syncable/Component/ComponentContent.ts create mode 100644 packages/models/src/Domain/Syncable/Component/ComponentMutator.ts create mode 100644 packages/models/src/Domain/Syncable/Component/PackageInfo.ts create mode 100644 packages/models/src/Domain/Syncable/Component/index.ts create mode 100644 packages/models/src/Domain/Syncable/Editor/Editor.ts create mode 100644 packages/models/src/Domain/Syncable/Editor/index.ts create mode 100644 packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepo.ts create mode 100644 packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepoMutator.ts create mode 100644 packages/models/src/Domain/Syncable/FeatureRepo/index.ts create mode 100644 packages/models/src/Domain/Syncable/File/File.spec.ts create mode 100644 packages/models/src/Domain/Syncable/File/File.ts create mode 100644 packages/models/src/Domain/Syncable/File/FileMetadata.ts create mode 100644 packages/models/src/Domain/Syncable/File/FileMutator.ts create mode 100644 packages/models/src/Domain/Syncable/File/FileProtocolV1.ts create mode 100644 packages/models/src/Domain/Syncable/File/index.ts create mode 100644 packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts create mode 100644 packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyMutatorInterface.ts create mode 100644 packages/models/src/Domain/Syncable/Note/Note.spec.ts create mode 100644 packages/models/src/Domain/Syncable/Note/Note.ts create mode 100644 packages/models/src/Domain/Syncable/Note/NoteContent.ts create mode 100644 packages/models/src/Domain/Syncable/Note/NoteMutator.ts create mode 100644 packages/models/src/Domain/Syncable/Note/index.ts create mode 100644 packages/models/src/Domain/Syncable/SmartView/SmartView.ts create mode 100644 packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts create mode 100644 packages/models/src/Domain/Syncable/SmartView/index.ts create mode 100644 packages/models/src/Domain/Syncable/Tag/Tag.spec.ts create mode 100644 packages/models/src/Domain/Syncable/Tag/Tag.ts create mode 100644 packages/models/src/Domain/Syncable/Tag/TagMutator.spec.ts create mode 100644 packages/models/src/Domain/Syncable/Tag/TagMutator.ts create mode 100644 packages/models/src/Domain/Syncable/Tag/index.ts create mode 100644 packages/models/src/Domain/Syncable/Theme/Theme.ts create mode 100644 packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts create mode 100644 packages/models/src/Domain/Syncable/Theme/index.ts create mode 100644 packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts create mode 100644 packages/models/src/Domain/Syncable/UserPrefs/UserPrefs.ts create mode 100644 packages/models/src/Domain/Syncable/UserPrefs/UserPrefsMutator.ts create mode 100644 packages/models/src/Domain/Syncable/UserPrefs/index.ts create mode 100644 packages/models/src/Domain/Utilities/Item/FindItem.ts create mode 100644 packages/models/src/Domain/Utilities/Item/ItemContentsDiffer.ts create mode 100644 packages/models/src/Domain/Utilities/Item/ItemContentsEqual.ts create mode 100644 packages/models/src/Domain/Utilities/Item/ItemGenerator.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/ConditionalPayloadType.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/CopyPayloadWithContentOverride.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/CreatePayload.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/FindPayload.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/PayloadContentsEqual.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/PayloadSplit.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/PayloadsByAlternatingUuid.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts create mode 100644 packages/models/src/Domain/Utilities/Payload/PayloadsByUpdatingReferencingPayloadReferences.ts create mode 100644 packages/models/src/Domain/Utilities/Test/SpecUtils.ts create mode 100644 packages/models/src/Domain/index.ts create mode 100644 packages/models/src/index.ts create mode 100644 packages/models/tsconfig.json diff --git a/.gitignore b/.gitignore index fab97a001..411b3e349 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ packages/filepicker/dist packages/features/dist packages/encryption/dist packages/files/dist +packages/models/dist **/.pnp.* **/.yarn/* diff --git a/.yarn/cache/@standardnotes-models-npm-1.11.12-d07d5ebeb6-636897db97.zip b/.yarn/cache/@standardnotes-models-npm-1.11.12-d07d5ebeb6-636897db97.zip deleted file mode 100644 index 70a5209e312c9e9fb525ee4116afd94aecf1ee53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232094 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

>Zh(aNQjD^34!)b@V#~>9tnR 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 PG}~0%MSlhVyvOr@(PEJt diff --git a/.yarn/cache/@standardnotes-models-npm-1.11.13-8272aa4de5-063f4382b8.zip b/.yarn/cache/@standardnotes-models-npm-1.11.13-8272aa4de5-063f4382b8.zip deleted file mode 100644 index a96f6c1ff523e7dcf91867122210ad1cc05d05b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232109 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

>Zh(aNQjD^34!)b@V#~>9tnR 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 T*risha?HeC^f|X+@Pqyj=16.0.0 <17.0.0" + }, + "description": "Models used in SNJS library", + "main": "dist/index.js", + "author": "Standard Notes", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "license": "AGPL-3.0-or-later", + "scripts": { + "clean": "rm -fr dist", + "prestart": "yarn clean", + "start": "tsc -p tsconfig.json --watch", + "prebuild": "yarn clean", + "build": "tsc -p tsconfig.json", + "lint": "eslint . --ext .ts", + "test:unit": "jest" + }, + "devDependencies": { + "@types/jest": "^27.4.1", + "@types/lodash": "^4.14.182", + "@typescript-eslint/eslint-plugin": "^5.30.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^27.5.1", + "ts-jest": "^27.1.3" + }, + "dependencies": { + "@standardnotes/common": "^1.23.1", + "@standardnotes/features": "workspace:*", + "@standardnotes/responses": "^1.6.39", + "@standardnotes/utils": "^1.6.12", + "lodash": "^4.17.21", + "reflect-metadata": "^0.1.13" + } +} diff --git a/packages/models/src/Domain/Abstract/Content/ItemContent.ts b/packages/models/src/Domain/Abstract/Content/ItemContent.ts new file mode 100644 index 000000000..c8d35f700 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Content/ItemContent.ts @@ -0,0 +1,51 @@ +import { Uuid } from '@standardnotes/common' +import { AppData, DefaultAppDomain } from '../Item/Types/DefaultAppDomain' +import { ContentReference } from '../Reference/ContentReference' +import { AppDataField } from '../Item/Types/AppDataField' + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SpecializedContent {} + +export interface ItemContent { + references: ContentReference[] + conflict_of?: Uuid + protected?: boolean + trashed?: boolean + pinned?: boolean + archived?: boolean + locked?: boolean + appData?: AppData +} + +/** + * Modifies the input object to fill in any missing required values from the + * content body. + */ + +export function FillItemContent(content: Partial): C { + if (!content.references) { + content.references = [] + } + + if (!content.appData) { + content.appData = { + [DefaultAppDomain]: {}, + } + } + + if (!content.appData[DefaultAppDomain]) { + content.appData[DefaultAppDomain] = {} + } + + if (!content.appData[DefaultAppDomain][AppDataField.UserModifiedDate]) { + content.appData[DefaultAppDomain][AppDataField.UserModifiedDate] = `${new Date()}` + } + + return content as C +} + +export function FillItemContentSpecialized( + content: S, +): C { + return FillItemContent(content) +} diff --git a/packages/models/src/Domain/Abstract/Contextual/BackupFile.ts b/packages/models/src/Domain/Abstract/Contextual/BackupFile.ts new file mode 100644 index 000000000..87b4bc6be --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/BackupFile.ts @@ -0,0 +1,60 @@ +import { Uuid } from '@standardnotes/common' +import { ContextPayload } from './ContextPayload' +import { ItemContent } from '../Content/ItemContent' +import { DecryptedTransferPayload, EncryptedTransferPayload } from '../TransferPayload' + +export interface BackupFileEncryptedContextualPayload extends ContextPayload { + auth_hash?: string + content: string + created_at_timestamp: number + created_at: Date + duplicate_of?: Uuid + enc_item_key: string + items_key_id: string | undefined + updated_at: Date + updated_at_timestamp: number +} + +export interface BackupFileDecryptedContextualPayload extends ContextPayload { + content: C + created_at_timestamp: number + created_at: Date + duplicate_of?: Uuid + updated_at: Date + updated_at_timestamp: number +} + +export function CreateEncryptedBackupFileContextPayload( + fromPayload: EncryptedTransferPayload, +): BackupFileEncryptedContextualPayload { + return { + auth_hash: fromPayload.auth_hash, + content_type: fromPayload.content_type, + content: fromPayload.content, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: false, + duplicate_of: fromPayload.duplicate_of, + enc_item_key: fromPayload.enc_item_key, + items_key_id: fromPayload.items_key_id, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + } +} + +export function CreateDecryptedBackupFileContextPayload( + fromPayload: DecryptedTransferPayload, +): BackupFileDecryptedContextualPayload { + return { + content_type: fromPayload.content_type, + content: fromPayload.content, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: false, + duplicate_of: fromPayload.duplicate_of, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/ComponentCreate.ts b/packages/models/src/Domain/Abstract/Contextual/ComponentCreate.ts new file mode 100644 index 000000000..f44589dc9 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/ComponentCreate.ts @@ -0,0 +1,24 @@ +import { ItemContent } from '../Content/ItemContent' +import { DecryptedTransferPayload } from '../TransferPayload' +import { ContextPayload } from './ContextPayload' + +/** + * Represents a payload with permissible fields for when a + * component wants to create a new item + */ +export interface ComponentCreateContextualPayload extends ContextPayload { + content: C + created_at?: Date +} + +export function createComponentCreatedContextPayload( + fromPayload: DecryptedTransferPayload, +): ComponentCreateContextualPayload { + return { + content_type: fromPayload.content_type, + content: fromPayload.content, + created_at: fromPayload.created_at, + deleted: false, + uuid: fromPayload.uuid, + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/ComponentRetrieved.ts b/packages/models/src/Domain/Abstract/Contextual/ComponentRetrieved.ts new file mode 100644 index 000000000..a6d7baf35 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/ComponentRetrieved.ts @@ -0,0 +1,24 @@ +import { ItemContent } from '../Content/ItemContent' +import { DecryptedTransferPayload } from '../TransferPayload' +import { ContextPayload } from './ContextPayload' + +/** + * Represents a payload with permissible fields for when a + * payload is retrieved from a component for saving + */ +export interface ComponentRetrievedContextualPayload extends ContextPayload { + content: C + created_at?: Date +} + +export function CreateComponentRetrievedContextPayload( + fromPayload: DecryptedTransferPayload, +): ComponentRetrievedContextualPayload { + return { + content_type: fromPayload.content_type, + content: fromPayload.content, + created_at: fromPayload.created_at, + deleted: false, + uuid: fromPayload.uuid, + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts b/packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts new file mode 100644 index 000000000..a3cd11887 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts @@ -0,0 +1,9 @@ +import { ContentType } from '@standardnotes/common' +import { ItemContent } from '../Content/ItemContent' + +export interface ContextPayload { + uuid: string + content_type: ContentType + content: C | string | undefined + deleted: boolean +} diff --git a/packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts b/packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts new file mode 100644 index 000000000..54c038db7 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts @@ -0,0 +1,25 @@ +import { ServerItemResponse } from '@standardnotes/responses' +import { isCorruptTransferPayload, isEncryptedTransferPayload } from '../TransferPayload' + +export interface FilteredServerItem extends ServerItemResponse { + __passed_filter__: true +} + +export function CreateFilteredServerItem(item: ServerItemResponse): FilteredServerItem { + return { + ...item, + __passed_filter__: true, + } +} + +export function FilterDisallowedRemotePayloadsAndMap(payloads: ServerItemResponse[]): FilteredServerItem[] { + return payloads.filter(isRemotePayloadAllowed).map(CreateFilteredServerItem) +} + +export function isRemotePayloadAllowed(payload: ServerItemResponse): boolean { + if (isCorruptTransferPayload(payload)) { + return false + } + + return isEncryptedTransferPayload(payload) || payload.content == undefined +} diff --git a/packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts b/packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts new file mode 100644 index 000000000..d37239301 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts @@ -0,0 +1,107 @@ +import { Uuid } from '@standardnotes/common' +import { ContextPayload } from './ContextPayload' +import { ItemContent } from '../Content/ItemContent' +import { DecryptedPayloadInterface, DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload' +import { useBoolean } from '@standardnotes/utils' +import { EncryptedTransferPayload, isEncryptedTransferPayload } from '../TransferPayload' + +export function isEncryptedLocalStoragePayload( + p: LocalStorageEncryptedContextualPayload | LocalStorageDecryptedContextualPayload, +): p is LocalStorageEncryptedContextualPayload { + return isEncryptedTransferPayload(p as EncryptedTransferPayload) +} + +export interface LocalStorageEncryptedContextualPayload extends ContextPayload { + auth_hash?: string + auth_params?: unknown + content: string + deleted: false + created_at_timestamp: number + created_at: Date + dirty: boolean + duplicate_of: Uuid | undefined + enc_item_key: string + errorDecrypting: boolean + items_key_id: string | undefined + updated_at_timestamp: number + updated_at: Date + waitingForKey: boolean +} + +export interface LocalStorageDecryptedContextualPayload extends ContextPayload { + content: C + created_at_timestamp: number + created_at: Date + deleted: false + dirty: boolean + duplicate_of?: Uuid + updated_at_timestamp: number + updated_at: Date +} + +export interface LocalStorageDeletedContextualPayload extends ContextPayload { + content: undefined + created_at_timestamp: number + created_at: Date + deleted: true + dirty: true + duplicate_of?: Uuid + updated_at_timestamp: number + updated_at: Date +} + +export function CreateEncryptedLocalStorageContextPayload( + fromPayload: EncryptedPayloadInterface, +): LocalStorageEncryptedContextualPayload { + return { + auth_hash: fromPayload.auth_hash, + content_type: fromPayload.content_type, + content: fromPayload.content, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: false, + dirty: fromPayload.dirty != undefined ? fromPayload.dirty : false, + duplicate_of: fromPayload.duplicate_of, + enc_item_key: fromPayload.enc_item_key, + errorDecrypting: fromPayload.errorDecrypting, + items_key_id: fromPayload.items_key_id, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + waitingForKey: fromPayload.waitingForKey, + } +} + +export function CreateDecryptedLocalStorageContextPayload( + fromPayload: DecryptedPayloadInterface, +): LocalStorageDecryptedContextualPayload { + return { + content_type: fromPayload.content_type, + content: fromPayload.content, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: false, + duplicate_of: fromPayload.duplicate_of, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + dirty: useBoolean(fromPayload.dirty, false), + } +} + +export function CreateDeletedLocalStorageContextPayload( + fromPayload: DeletedPayloadInterface, +): LocalStorageDeletedContextualPayload { + return { + content_type: fromPayload.content_type, + content: undefined, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: true, + dirty: true, + duplicate_of: fromPayload.duplicate_of, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/OfflineSyncPush.ts b/packages/models/src/Domain/Abstract/Contextual/OfflineSyncPush.ts new file mode 100644 index 000000000..e358425a9 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/OfflineSyncPush.ts @@ -0,0 +1,41 @@ +import { Uuid } from '@standardnotes/common' +import { ItemContent } from '../Content/ItemContent' +import { DecryptedPayloadInterface, DeletedPayloadInterface, isDecryptedPayload } from '../Payload' +import { ContextPayload } from './ContextPayload' + +export interface OfflineSyncPushContextualPayload extends ContextPayload { + content: ItemContent | undefined + created_at_timestamp: number + created_at: Date + duplicate_of?: Uuid + updated_at_timestamp: number + updated_at: Date +} + +export function CreateOfflineSyncPushContextPayload( + fromPayload: DecryptedPayloadInterface | DeletedPayloadInterface, +): OfflineSyncPushContextualPayload { + const base: OfflineSyncPushContextualPayload = { + content: undefined, + content_type: fromPayload.content_type, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: false, + duplicate_of: fromPayload.duplicate_of, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + } + + if (isDecryptedPayload(fromPayload)) { + return { + ...base, + content: fromPayload.content, + } + } else { + return { + ...base, + deleted: fromPayload.deleted, + } + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/OfflineSyncSaved.ts b/packages/models/src/Domain/Abstract/Contextual/OfflineSyncSaved.ts new file mode 100644 index 000000000..e6d9b64aa --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/OfflineSyncSaved.ts @@ -0,0 +1,30 @@ +import { ContentType } from '@standardnotes/common' +import { DecryptedPayloadInterface, DeletedPayloadInterface, isDeletedPayload } from '../Payload' + +/** + * The saved sync item payload represents the payload we want to map + * when mapping saved_items from the server or local sync mechanism. We only want to map the + * updated_at value the server returns for the item, and basically + * nothing else. + */ +export interface OfflineSyncSavedContextualPayload { + content_type: ContentType + created_at_timestamp: number + deleted: boolean + updated_at_timestamp?: number + updated_at: Date + uuid: string +} + +export function CreateOfflineSyncSavedPayload( + fromPayload: DecryptedPayloadInterface | DeletedPayloadInterface, +): OfflineSyncSavedContextualPayload { + return { + content_type: fromPayload.content_type, + created_at_timestamp: fromPayload.created_at_timestamp, + deleted: isDeletedPayload(fromPayload), + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/ServerSyncPush.ts b/packages/models/src/Domain/Abstract/Contextual/ServerSyncPush.ts new file mode 100644 index 000000000..6f8c7801f --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/ServerSyncPush.ts @@ -0,0 +1,50 @@ +import { Uuid } from '@standardnotes/common' +import { DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload' +import { ContextPayload } from './ContextPayload' + +export interface ServerSyncPushContextualPayload extends ContextPayload { + auth_hash?: string + content: string | undefined + created_at_timestamp: number + created_at: Date + duplicate_of?: Uuid + enc_item_key?: string + items_key_id?: string + updated_at_timestamp: number + updated_at: Date +} + +export function CreateEncryptedServerSyncPushPayload( + fromPayload: EncryptedPayloadInterface, +): ServerSyncPushContextualPayload { + return { + content_type: fromPayload.content_type, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: false, + duplicate_of: fromPayload.duplicate_of, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + content: fromPayload.content, + enc_item_key: fromPayload.enc_item_key, + items_key_id: fromPayload.items_key_id, + auth_hash: fromPayload.auth_hash, + } +} + +export function CreateDeletedServerSyncPushPayload( + fromPayload: DeletedPayloadInterface, +): ServerSyncPushContextualPayload { + return { + content_type: fromPayload.content_type, + created_at_timestamp: fromPayload.created_at_timestamp, + created_at: fromPayload.created_at, + deleted: true, + duplicate_of: fromPayload.duplicate_of, + updated_at_timestamp: fromPayload.updated_at_timestamp, + updated_at: fromPayload.updated_at, + uuid: fromPayload.uuid, + content: undefined, + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/ServerSyncSaved.ts b/packages/models/src/Domain/Abstract/Contextual/ServerSyncSaved.ts new file mode 100644 index 000000000..d77284615 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/ServerSyncSaved.ts @@ -0,0 +1,31 @@ +import { useBoolean } from '@standardnotes/utils' +import { FilteredServerItem } from './FilteredServerItem' +import { ContentType } from '@standardnotes/common' + +/** + * The saved sync item payload represents the payload we want to map + * when mapping saved_items from the server. We only want to map the + * updated_at value the server returns for the item, and basically + * nothing else. + */ +export interface ServerSyncSavedContextualPayload { + content_type: ContentType + created_at_timestamp: number + created_at: Date + deleted: boolean + updated_at_timestamp: number + updated_at: Date + uuid: string +} + +export function CreateServerSyncSavedPayload(from: FilteredServerItem): ServerSyncSavedContextualPayload { + return { + content_type: from.content_type, + created_at_timestamp: from.created_at_timestamp, + created_at: from.created_at, + deleted: useBoolean(from.deleted, false), + updated_at_timestamp: from.updated_at_timestamp, + updated_at: from.updated_at, + uuid: from.uuid, + } +} diff --git a/packages/models/src/Domain/Abstract/Contextual/SessionHistory.ts b/packages/models/src/Domain/Abstract/Contextual/SessionHistory.ts new file mode 100644 index 000000000..0de13f34d --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/SessionHistory.ts @@ -0,0 +1,7 @@ +import { ItemContent } from '../Content/ItemContent' +import { ContextPayload } from './ContextPayload' + +export interface SessionHistoryContextualPayload extends ContextPayload { + content: C + updated_at: Date +} diff --git a/packages/models/src/Domain/Abstract/Contextual/index.ts b/packages/models/src/Domain/Abstract/Contextual/index.ts new file mode 100644 index 000000000..b6a0ab3e6 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Contextual/index.ts @@ -0,0 +1,10 @@ +export * from './ComponentCreate' +export * from './ComponentRetrieved' +export * from './BackupFile' +export * from './LocalStorage' +export * from './OfflineSyncPush' +export * from './OfflineSyncSaved' +export * from './ServerSyncPush' +export * from './SessionHistory' +export * from './ServerSyncSaved' +export * from './FilteredServerItem' diff --git a/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts b/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts new file mode 100644 index 000000000..21740cd11 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts @@ -0,0 +1,122 @@ +import { dateToLocalizedString, useBoolean } from '@standardnotes/utils' +import { Uuid } from '@standardnotes/common' +import { DecryptedTransferPayload } from './../../TransferPayload/Interfaces/DecryptedTransferPayload' +import { AppDataField } from '../Types/AppDataField' +import { ComponentDataDomain, DefaultAppDomain } from '../Types/DefaultAppDomain' +import { DecryptedItemInterface } from '../Interfaces/DecryptedItem' +import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload' +import { GenericItem } from './GenericItem' +import { ItemContent } from '../../Content/ItemContent' +import { ItemContentsEqual } from '../../../Utilities/Item/ItemContentsEqual' +import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey' +import { ContentReference } from '../../Reference/ContentReference' + +export class DecryptedItem + extends GenericItem> + implements DecryptedItemInterface +{ + public readonly conflictOf?: Uuid + public readonly protected: boolean = false + public readonly trashed: boolean = false + public readonly pinned: boolean = false + public readonly archived: boolean = false + public readonly locked: boolean = false + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + this.conflictOf = payload.content.conflict_of + + const userModVal = this.getAppDomainValueWithDefault(AppDataField.UserModifiedDate, this.serverUpdatedAt || 0) + + this.userModifiedDate = new Date(userModVal as number | Date) + this.updatedAtString = dateToLocalizedString(this.userModifiedDate) + this.protected = useBoolean(this.payload.content.protected, false) + this.trashed = useBoolean(this.payload.content.trashed, false) + this.pinned = this.getAppDomainValueWithDefault(AppDataField.Pinned, false) + this.archived = this.getAppDomainValueWithDefault(AppDataField.Archived, false) + this.locked = this.getAppDomainValueWithDefault(AppDataField.Locked, false) + } + + public static DefaultAppDomain() { + return DefaultAppDomain + } + + get content() { + return this.payload.content + } + + get references(): ContentReference[] { + return this.payload.content.references || [] + } + + public isReferencingItem(item: DecryptedItemInterface): boolean { + return this.references.find((r) => r.uuid === item.uuid) != undefined + } + + /** + * Inside of content is a record called `appData` (which should have been called `domainData`). + * It was named `appData` as a way to indicate that it can house data for multiple apps. + * Each key of appData is a domain string, which was originally designed + * to allow for multiple 3rd party apps who share access to the same data to store data + * in an isolated location. This design premise is antiquited and no longer pursued, + * however we continue to use it as not to uncesesarily create a large data migration + * that would require users to sync all their data. + * + * domainData[DomainKey] will give you another Record. + * + * Currently appData['org.standardnotes.sn'] returns an object of type AppData. + * And appData['org.standardnotes.sn.components] returns an object of type ComponentData + */ + public getDomainData( + domain: typeof ComponentDataDomain | typeof DefaultAppDomain, + ): undefined | Record { + const domainData = this.payload.content.appData + if (!domainData) { + return undefined + } + const data = domainData[domain] + return data + } + + public getAppDomainValue(key: AppDataField | PrefKey): T | undefined { + const appData = this.getDomainData(DefaultAppDomain) + return appData?.[key] as T + } + + public getAppDomainValueWithDefault(key: AppDataField | PrefKey, defaultValue: D): T { + const appData = this.getDomainData(DefaultAppDomain) + return (appData?.[key] as T) || defaultValue + } + + public override payloadRepresentation(override?: Partial>): DecryptedPayloadInterface { + return this.payload.copy(override) + } + + /** + * During sync conflicts, when determing whether to create a duplicate for an item, + * we can omit keys that have no meaningful weight and can be ignored. For example, + * if one component has active = true and another component has active = false, + * it would be needless to duplicate them, so instead we ignore that value. + */ + public contentKeysToIgnoreWhenCheckingEquality(): (keyof C)[] { + return ['conflict_of'] + } + + /** Same as `contentKeysToIgnoreWhenCheckingEquality`, but keys inside appData[Item.AppDomain] */ + public appDataContentKeysToIgnoreWhenCheckingEquality(): AppDataField[] { + return [AppDataField.UserModifiedDate] + } + + public getContentCopy() { + return JSON.parse(JSON.stringify(this.content)) + } + + public isItemContentEqualWith(otherItem: DecryptedItemInterface) { + return ItemContentsEqual( + this.payload.content, + otherItem.payload.content, + this.contentKeysToIgnoreWhenCheckingEquality(), + this.appDataContentKeysToIgnoreWhenCheckingEquality(), + ) + } +} diff --git a/packages/models/src/Domain/Abstract/Item/Implementations/DeletedItem.ts b/packages/models/src/Domain/Abstract/Item/Implementations/DeletedItem.ts new file mode 100644 index 000000000..383a5cfd5 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Implementations/DeletedItem.ts @@ -0,0 +1,18 @@ +import { GenericItem } from './GenericItem' +import { DeletedPayloadInterface } from '../../Payload' +import { DeletedItemInterface } from '../Interfaces/DeletedItem' +import { DeletedTransferPayload } from '../../TransferPayload' + +export class DeletedItem extends GenericItem implements DeletedItemInterface { + deleted: true + content: undefined + + constructor(payload: DeletedPayloadInterface) { + super(payload) + this.deleted = true + } + + public override payloadRepresentation(override?: Partial): DeletedPayloadInterface { + return this.payload.copy(override) + } +} diff --git a/packages/models/src/Domain/Abstract/Item/Implementations/EncryptedItem.ts b/packages/models/src/Domain/Abstract/Item/Implementations/EncryptedItem.ts new file mode 100644 index 000000000..97a76ac3f --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Implementations/EncryptedItem.ts @@ -0,0 +1,34 @@ +import { EncryptedTransferPayload } from './../../TransferPayload/Interfaces/EncryptedTransferPayload' +import { EncryptedItemInterface } from '../Interfaces/EncryptedItem' +import { EncryptedPayloadInterface } from '../../Payload/Interfaces/EncryptedPayload' +import { GenericItem } from './GenericItem' + +export class EncryptedItem extends GenericItem implements EncryptedItemInterface { + constructor(payload: EncryptedPayloadInterface) { + super(payload) + } + + get version() { + return this.payload.version + } + + public override payloadRepresentation(override?: Partial): EncryptedPayloadInterface { + return this.payload.copy(override) + } + + get errorDecrypting() { + return this.payload.errorDecrypting + } + + get waitingForKey() { + return this.payload.waitingForKey + } + + get content() { + return this.payload.content + } + + get auth_hash() { + return this.payload.auth_hash + } +} diff --git a/packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts b/packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts new file mode 100644 index 000000000..42a1c7b72 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts @@ -0,0 +1,189 @@ +import { ContentType, Uuid } from '@standardnotes/common' +import { dateToLocalizedString, deepFreeze } from '@standardnotes/utils' +import { TransferPayload } from './../../TransferPayload/Interfaces/TransferPayload' +import { ItemContentsDiffer } from '../../../Utilities/Item/ItemContentsDiffer' +import { ItemInterface } from '../Interfaces/ItemInterface' +import { PayloadSource } from '../../Payload/Types/PayloadSource' +import { ConflictStrategy } from '../Types/ConflictStrategy' +import { PredicateInterface } from '../../../Runtime/Predicate/Interface' +import { SingletonStrategy } from '../Types/SingletonStrategy' +import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface' +import { HistoryEntryInterface } from '../../../Runtime/History/HistoryEntryInterface' +import { isDecryptedItem, isDeletedItem, isEncryptedErroredItem } from '../Interfaces/TypeCheck' + +export abstract class GenericItem

implements ItemInterface

{ + payload: P + public readonly duplicateOf?: Uuid + public readonly createdAtString?: string + public updatedAtString?: string + public userModifiedDate: Date + + constructor(payload: P) { + this.payload = payload + this.duplicateOf = payload.duplicate_of + this.createdAtString = this.created_at && dateToLocalizedString(this.created_at) + this.userModifiedDate = this.serverUpdatedAt || new Date() + this.updatedAtString = dateToLocalizedString(this.userModifiedDate) + + const timeToAllowSubclassesToFinishConstruction = 0 + setTimeout(() => { + deepFreeze(this) + }, timeToAllowSubclassesToFinishConstruction) + } + + get uuid() { + return this.payload.uuid + } + + get content_type(): ContentType { + return this.payload.content_type + } + + get created_at() { + return this.payload.created_at + } + + /** + * The date timestamp the server set for this item upon it being synced + * Undefined if never synced to a remote server. + */ + public get serverUpdatedAt(): Date { + return this.payload.serverUpdatedAt + } + + public get serverUpdatedAtTimestamp(): number | undefined { + return this.payload.updated_at_timestamp + } + + /** @deprecated Use serverUpdatedAt instead */ + public get updated_at(): Date | undefined { + return this.serverUpdatedAt + } + + get dirty() { + return this.payload.dirty + } + + get lastSyncBegan() { + return this.payload.lastSyncBegan + } + + get lastSyncEnd() { + return this.payload.lastSyncEnd + } + + get duplicate_of() { + return this.payload.duplicate_of + } + + public payloadRepresentation(override?: Partial): P { + return this.payload.copy(override) + } + + /** Whether the item has never been synced to a server */ + public get neverSynced(): boolean { + return !this.serverUpdatedAt || this.serverUpdatedAt.getTime() === 0 + } + + /** + * Subclasses can override this getter to return true if they want only + * one of this item to exist, depending on custom criteria. + */ + public get isSingleton(): boolean { + return false + } + + /** The predicate by which singleton items should be unique */ + public singletonPredicate(): PredicateInterface { + throw 'Must override SNItem.singletonPredicate' + } + + public get singletonStrategy(): SingletonStrategy { + return SingletonStrategy.KeepEarliest + } + + /** + * Subclasses can override this method and provide their own opinion on whether + * they want to be duplicated. For example, if this.content.x = 12 and + * item.content.x = 13, this function can be overriden to always return + * ConflictStrategy.KeepBase to say 'don't create a duplicate at all, the + * change is not important.' + * + * In the default implementation, we create a duplicate if content differs. + * However, if they only differ by references, we KEEP_LEFT_MERGE_REFS. + * + * Left returns to our current item, and Right refers to the incoming item. + */ + public strategyWhenConflictingWithItem( + item: ItemInterface, + previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + if (isEncryptedErroredItem(this)) { + return ConflictStrategy.KeepBaseDuplicateApply + } + + if (this.isSingleton) { + return ConflictStrategy.KeepBase + } + + if (isDeletedItem(this)) { + return ConflictStrategy.KeepApply + } + + if (isDeletedItem(item)) { + if (this.payload.source === PayloadSource.FileImport) { + return ConflictStrategy.KeepBase + } + return ConflictStrategy.KeepApply + } + + if (!isDecryptedItem(item) || !isDecryptedItem(this)) { + return ConflictStrategy.KeepBaseDuplicateApply + } + + const contentDiffers = ItemContentsDiffer(this, item) + if (!contentDiffers) { + return ConflictStrategy.KeepApply + } + + const itemsAreDifferentExcludingRefs = ItemContentsDiffer(this, item, ['references']) + if (itemsAreDifferentExcludingRefs) { + if (previousRevision) { + /** + * If previousRevision.content === incomingValue.content, this means the + * change that was rejected by the server is in fact a legitimate change, + * because the value the client had previously matched with the server's, + * and this new change is being built on top of that state, and should therefore + * be chosen as the winner, with no need for a conflict. + */ + if (!ItemContentsDiffer(previousRevision.itemFromPayload(), item)) { + return ConflictStrategy.KeepBase + } + } + const twentySeconds = 20_000 + if ( + /** + * If the incoming item comes from an import, treat it as + * less important than the existing one. + */ + item.payload.source === PayloadSource.FileImport || + /** + * If the user is actively editing our item, duplicate the incoming item + * to avoid creating surprises in the client's UI. + */ + Date.now() - this.userModifiedDate.getTime() < twentySeconds + ) { + return ConflictStrategy.KeepBaseDuplicateApply + } else { + return ConflictStrategy.DuplicateBaseKeepApply + } + } else { + /** Only the references have changed; merge them. */ + return ConflictStrategy.KeepBaseMergeRefs + } + } + + public satisfiesPredicate(predicate: PredicateInterface): boolean { + return predicate.matchesItem(this) + } +} diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts new file mode 100644 index 000000000..7bd838f6b --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts @@ -0,0 +1,45 @@ +import { Uuid } from '@standardnotes/common' +import { AppDataField } from '../Types/AppDataField' +import { ComponentDataDomain, DefaultAppDomain } from '../Types/DefaultAppDomain' +import { ContentReference } from '../../Reference/ContentReference' +import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey' +import { ItemContent } from '../../Content/ItemContent' +import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload' +import { ItemInterface } from './ItemInterface' +import { SortableItem } from '../../../Runtime/Collection/CollectionSort' +import { DecryptedTransferPayload } from '../../TransferPayload/Interfaces/DecryptedTransferPayload' +import { SearchableItem } from '../../../Runtime/Display' + +export interface DecryptedItemInterface + extends ItemInterface>, + SortableItem, + SearchableItem { + readonly content: C + readonly conflictOf?: Uuid + readonly duplicateOf?: Uuid + readonly protected: boolean + readonly trashed: boolean + readonly pinned: boolean + readonly archived: boolean + readonly locked: boolean + readonly userModifiedDate: Date + readonly references: ContentReference[] + + getAppDomainValueWithDefault(key: AppDataField | PrefKey, defaultValue: D): T + + getAppDomainValue(key: AppDataField | PrefKey): T | undefined + + isItemContentEqualWith(otherItem: DecryptedItemInterface): boolean + + payloadRepresentation(override?: Partial>): DecryptedPayloadInterface + + isReferencingItem(item: DecryptedItemInterface): boolean + + getDomainData(domain: typeof ComponentDataDomain | typeof DefaultAppDomain): undefined | Record + + contentKeysToIgnoreWhenCheckingEquality(): (keyof C)[] + + appDataContentKeysToIgnoreWhenCheckingEquality(): AppDataField[] + + getContentCopy(): C +} diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/DeletedItem.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/DeletedItem.ts new file mode 100644 index 000000000..f6d6d2010 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/DeletedItem.ts @@ -0,0 +1,7 @@ +import { DeletedPayloadInterface } from './../../Payload/Interfaces/DeletedPayload' +import { ItemInterface } from './ItemInterface' + +export interface DeletedItemInterface extends ItemInterface { + readonly deleted: true + readonly content: undefined +} diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/EncryptedItem.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/EncryptedItem.ts new file mode 100644 index 000000000..5c3c3071e --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/EncryptedItem.ts @@ -0,0 +1,11 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { EncryptedPayloadInterface } from '../../Payload/Interfaces/EncryptedPayload' +import { ItemInterface } from './ItemInterface' + +export interface EncryptedItemInterface extends ItemInterface { + content: string + version: ProtocolVersion + errorDecrypting: boolean + waitingForKey?: boolean + auth_hash?: string +} diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts new file mode 100644 index 000000000..d6087b83c --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts @@ -0,0 +1,41 @@ +import { Uuid, ContentType } from '@standardnotes/common' +import { TransferPayload } from './../../TransferPayload/Interfaces/TransferPayload' +import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface' +import { PredicateInterface } from '../../../Runtime/Predicate/Interface' +import { HistoryEntryInterface } from '../../../Runtime/History' +import { ConflictStrategy } from '../Types/ConflictStrategy' +import { SingletonStrategy } from '../Types/SingletonStrategy' + +export interface ItemInterface

{ + payload: P + readonly conflictOf?: Uuid + readonly duplicateOf?: Uuid + readonly createdAtString?: string + readonly updatedAtString?: string + + uuid: Uuid + + content_type: ContentType + created_at: Date + serverUpdatedAt: Date + serverUpdatedAtTimestamp: number | undefined + dirty: boolean | undefined + + lastSyncBegan: Date | undefined + lastSyncEnd: Date | undefined + neverSynced: boolean + + duplicate_of: string | undefined + isSingleton: boolean + updated_at: Date | undefined + + singletonPredicate(): PredicateInterface + + singletonStrategy: SingletonStrategy + + strategyWhenConflictingWithItem(item: ItemInterface, previousRevision?: HistoryEntryInterface): ConflictStrategy + + satisfiesPredicate(predicate: PredicateInterface): boolean + + payloadRepresentation(override?: Partial): P +} diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/TypeCheck.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/TypeCheck.ts new file mode 100644 index 000000000..e2d3cec27 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/TypeCheck.ts @@ -0,0 +1,31 @@ +import { EncryptedItemInterface } from './EncryptedItem' +import { DeletedItemInterface } from './DeletedItem' +import { ItemInterface } from './ItemInterface' +import { DecryptedItemInterface } from './DecryptedItem' +import { isDecryptedPayload, isDeletedPayload, isEncryptedPayload } from '../../Payload/Interfaces/TypeCheck' + +export function isDecryptedItem(item: ItemInterface): item is DecryptedItemInterface { + return isDecryptedPayload(item.payload) +} + +export function isEncryptedItem(item: ItemInterface): item is EncryptedItemInterface { + return isEncryptedPayload(item.payload) +} + +export function isNotEncryptedItem( + item: DecryptedItemInterface | DeletedItemInterface | EncryptedItemInterface, +): item is DecryptedItemInterface | DeletedItemInterface { + return !isEncryptedItem(item) +} + +export function isDeletedItem(item: ItemInterface): item is DeletedItemInterface { + return isDeletedPayload(item.payload) +} + +export function isDecryptedOrDeletedItem(item: ItemInterface): item is DecryptedItemInterface | DeletedItemInterface { + return isDecryptedItem(item) || isDeletedItem(item) +} + +export function isEncryptedErroredItem(item: ItemInterface): boolean { + return isEncryptedItem(item) && item.errorDecrypting === true +} diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/UnionTypes.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/UnionTypes.ts new file mode 100644 index 000000000..13332c525 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/UnionTypes.ts @@ -0,0 +1,9 @@ +import { ItemContent } from '../../Content/ItemContent' +import { DecryptedItemInterface } from './DecryptedItem' +import { DeletedItemInterface } from './DeletedItem' +import { EncryptedItemInterface } from './EncryptedItem' + +export type AnyItemInterface = + | EncryptedItemInterface + | DecryptedItemInterface + | DeletedItemInterface diff --git a/packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts b/packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts new file mode 100644 index 000000000..f6b3e7610 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts @@ -0,0 +1,145 @@ +import { DecryptedItemInterface } from './../Interfaces/DecryptedItem' +import { Copy } from '@standardnotes/utils' +import { MutationType } from '../Types/MutationType' +import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey' +import { Uuid } from '@standardnotes/common' +import { ItemContent } from '../../Content/ItemContent' +import { AppDataField } from '../Types/AppDataField' +import { DefaultAppDomain, DomainDataValueType, ItemDomainKey } from '../Types/DefaultAppDomain' +import { ItemMutator } from './ItemMutator' +import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload' +import { ItemInterface } from '../Interfaces/ItemInterface' +import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter' + +export class DecryptedItemMutator extends ItemMutator< + DecryptedPayloadInterface, + DecryptedItemInterface +> { + protected mutableContent: C + + constructor(item: DecryptedItemInterface, type: MutationType) { + super(item, type) + + const mutableCopy = Copy(this.immutablePayload.content) + this.mutableContent = mutableCopy + } + + public override getResult() { + if (this.type === MutationType.NonDirtying) { + return this.immutablePayload.copy({ + content: this.mutableContent, + }) + } + + if (this.type === MutationType.UpdateUserTimestamps) { + this.userModifiedDate = new Date() + } else { + const currentValue = this.immutableItem.userModifiedDate + if (!currentValue) { + this.userModifiedDate = new Date(this.immutableItem.serverUpdatedAt) + } + } + + const result = this.immutablePayload.copy({ + content: this.mutableContent, + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + }) + + return result + } + + public override setBeginSync(began: Date, globalDirtyIndex: number) { + this.immutablePayload = this.immutablePayload.copy({ + content: this.mutableContent, + lastSyncBegan: began, + globalDirtyIndexAtLastSync: globalDirtyIndex, + }) + } + + /** Not recommended to use as this might break item schema if used incorrectly */ + public setCustomContent(content: C): void { + this.mutableContent = Copy(content) + } + + public set userModifiedDate(date: Date) { + this.setAppDataItem(AppDataField.UserModifiedDate, date) + } + + public set conflictOf(conflictOf: Uuid | undefined) { + this.mutableContent.conflict_of = conflictOf + } + + public set protected(isProtected: boolean) { + this.mutableContent.protected = isProtected + } + + public set trashed(trashed: boolean) { + this.mutableContent.trashed = trashed + } + + public set pinned(pinned: boolean) { + this.setAppDataItem(AppDataField.Pinned, pinned) + } + + public set archived(archived: boolean) { + this.setAppDataItem(AppDataField.Archived, archived) + } + + public set locked(locked: boolean) { + this.setAppDataItem(AppDataField.Locked, locked) + } + + /** + * Overwrites the entirety of this domain's data with the data arg. + */ + public setDomainData(data: DomainDataValueType, domain: ItemDomainKey): void { + if (!this.mutableContent.appData) { + this.mutableContent.appData = { + [DefaultAppDomain]: {}, + } + } + + this.mutableContent.appData[domain] = data + } + + /** + * First gets the domain data for the input domain. + * Then sets data[key] = value + */ + public setDomainDataKey(key: keyof DomainDataValueType, value: unknown, domain: ItemDomainKey): void { + if (!this.mutableContent.appData) { + this.mutableContent.appData = { + [DefaultAppDomain]: {}, + } + } + + if (!this.mutableContent.appData[domain]) { + this.mutableContent.appData[domain] = {} + } + + const domainData = this.mutableContent.appData[domain] as DomainDataValueType + domainData[key] = value + } + + public setAppDataItem(key: AppDataField | PrefKey, value: unknown) { + this.setDomainDataKey(key, value, DefaultAppDomain) + } + + public e2ePendingRefactor_addItemAsRelationship(item: DecryptedItemInterface) { + const references = this.mutableContent.references || [] + if (!references.find((r) => r.uuid === item.uuid)) { + references.push({ + uuid: item.uuid, + content_type: item.content_type, + }) + } + this.mutableContent.references = references + } + + public removeItemAsRelationship(item: ItemInterface) { + let references = this.mutableContent.references || [] + references = references.filter((r) => r.uuid !== item.uuid) + this.mutableContent.references = references + } +} diff --git a/packages/models/src/Domain/Abstract/Item/Mutator/DeleteMutator.ts b/packages/models/src/Domain/Abstract/Item/Mutator/DeleteMutator.ts new file mode 100644 index 000000000..df0ff6d2d --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Mutator/DeleteMutator.ts @@ -0,0 +1,30 @@ +import { DeletedPayload } from './../../Payload/Implementations/DeletedPayload' +import { DeletedPayloadInterface, PayloadInterface } from '../../Payload' +import { ItemInterface } from '../Interfaces/ItemInterface' +import { ItemMutator } from './ItemMutator' +import { MutationType } from '../Types/MutationType' +import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter' + +export class DeleteItemMutator< + I extends ItemInterface = ItemInterface, +> extends ItemMutator { + public getDeletedResult(): DeletedPayloadInterface { + const dirtying = this.type !== MutationType.NonDirtying + const result = new DeletedPayload( + { + ...this.immutablePayload.ejected(), + deleted: true, + content: undefined, + dirty: dirtying ? true : this.immutablePayload.dirty, + dirtyIndex: dirtying ? getIncrementedDirtyIndex() : this.immutablePayload.dirtyIndex, + }, + this.immutablePayload.source, + ) + + return result + } + + public override getResult(): PayloadInterface { + throw Error('Must use getDeletedResult') + } +} diff --git a/packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts b/packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts new file mode 100644 index 000000000..2214a9d70 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts @@ -0,0 +1,65 @@ +import { MutationType } from '../Types/MutationType' +import { PayloadInterface } from '../../Payload' +import { ItemInterface } from '../Interfaces/ItemInterface' +import { TransferPayload } from '../../TransferPayload' +import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter' + +/** + * An item mutator takes in an item, and an operation, and returns the resulting payload. + * Subclasses of mutators can modify the content field directly, but cannot modify the payload directly. + * All changes to the payload must occur by copying the payload and reassigning its value. + */ +export class ItemMutator< + P extends PayloadInterface = PayloadInterface, + I extends ItemInterface

= ItemInterface

, +> { + public readonly immutableItem: I + protected immutablePayload: P + protected readonly type: MutationType + + constructor(item: I, type: MutationType) { + this.immutableItem = item + this.type = type + this.immutablePayload = item.payload + } + + public getUuid() { + return this.immutablePayload.uuid + } + + public getItem(): I { + return this.immutableItem + } + + public getResult(): P { + if (this.type === MutationType.NonDirtying) { + return this.immutablePayload.copy() + } + + const result = this.immutablePayload.copy({ + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + }) + + return result + } + + public setBeginSync(began: Date, globalDirtyIndex: number) { + this.immutablePayload = this.immutablePayload.copy({ + lastSyncBegan: began, + globalDirtyIndexAtLastSync: globalDirtyIndex, + }) + } + + public set errorDecrypting(_: boolean) { + throw Error('This method is no longer implemented') + } + + public set updated_at(_: Date) { + throw Error('This method is no longer implemented') + } + + public set updated_at_timestamp(_: number) { + throw Error('This method is no longer implemented') + } +} diff --git a/packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts b/packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts new file mode 100644 index 000000000..ea530b0c3 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts @@ -0,0 +1,13 @@ +export enum AppDataField { + Pinned = 'pinned', + Archived = 'archived', + Locked = 'locked', + UserModifiedDate = 'client_updated_at', + DefaultEditor = 'defaultEditor', + MobileRules = 'mobileRules', + NotAvailableOnMobile = 'notAvailableOnMobile', + MobileActive = 'mobileActive', + LastSize = 'lastSize', + PrefersPlainEditor = 'prefersPlainEditor', + ComponentInstallError = 'installError', +} diff --git a/packages/models/src/Domain/Abstract/Item/Types/ConflictStrategy.ts b/packages/models/src/Domain/Abstract/Item/Types/ConflictStrategy.ts new file mode 100644 index 000000000..4c711b8e2 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Types/ConflictStrategy.ts @@ -0,0 +1,7 @@ +export enum ConflictStrategy { + KeepBase = 1, + KeepApply = 2, + KeepBaseDuplicateApply = 3, + DuplicateBaseKeepApply = 4, + KeepBaseMergeRefs = 5, +} diff --git a/packages/models/src/Domain/Abstract/Item/Types/DefaultAppDomain.ts b/packages/models/src/Domain/Abstract/Item/Types/DefaultAppDomain.ts new file mode 100644 index 000000000..9382ca2ae --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Types/DefaultAppDomain.ts @@ -0,0 +1,17 @@ +import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey' +import { AppDataField } from './AppDataField' + +export const DefaultAppDomain = 'org.standardnotes.sn' +/* This domain will be used to save context item client data */ +export const ComponentDataDomain = 'org.standardnotes.sn.components' + +export type ItemDomainKey = typeof DefaultAppDomain | typeof ComponentDataDomain + +export type AppDomainValueType = Partial> +export type ComponentDomainValueType = Record +export type DomainDataValueType = AppDomainValueType | ComponentDomainValueType + +export type AppData = { + [DefaultAppDomain]: AppDomainValueType + [ComponentDataDomain]?: ComponentDomainValueType +} diff --git a/packages/models/src/Domain/Abstract/Item/Types/MutationType.ts b/packages/models/src/Domain/Abstract/Item/Types/MutationType.ts new file mode 100644 index 000000000..ba84e9623 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Types/MutationType.ts @@ -0,0 +1,14 @@ +export enum MutationType { + UpdateUserTimestamps = 1, + /** + * The item was changed as part of an internal operation, such as a migration, or, a user + * interaction that shouldn't modify timestamps (pinning, protecting, etc). + */ + NoUpdateUserTimestamps = 2, + /** + * The item was changed as part of an internal function that wishes to modify + * internal item properties, such as lastSyncBegan, without modifying the item's dirty + * state. By default all other mutation types will result in a dirtied result. + */ + NonDirtying = 3, +} diff --git a/packages/models/src/Domain/Abstract/Item/Types/SingletonStrategy.ts b/packages/models/src/Domain/Abstract/Item/Types/SingletonStrategy.ts new file mode 100644 index 000000000..7721db226 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/Types/SingletonStrategy.ts @@ -0,0 +1,3 @@ +export enum SingletonStrategy { + KeepEarliest = 1, +} diff --git a/packages/models/src/Domain/Abstract/Item/index.ts b/packages/models/src/Domain/Abstract/Item/index.ts new file mode 100644 index 000000000..99509324b --- /dev/null +++ b/packages/models/src/Domain/Abstract/Item/index.ts @@ -0,0 +1,29 @@ +export * from '../Reference/AnonymousReference' +export * from '../Reference/ContenteReferenceType' +export * from '../Reference/ContentReference' +export * from '../Reference/FileToNoteReference' +export * from '../Reference/Functions' +export * from '../Reference/LegacyAnonymousReference' +export * from '../Reference/LegacyTagToNoteReference' +export * from '../Reference/Reference' +export * from '../Reference/TagToParentTagReference' +export * from './Implementations/DecryptedItem' +export * from './Implementations/DecryptedItem' +export * from './Implementations/DeletedItem' +export * from './Implementations/EncryptedItem' +export * from './Implementations/GenericItem' +export * from './Interfaces/DecryptedItem' +export * from './Interfaces/DeletedItem' +export * from './Interfaces/EncryptedItem' +export * from './Interfaces/ItemInterface' +export * from './Interfaces/TypeCheck' +export * from './Mutator/DecryptedItemMutator' +export * from './Mutator/DeleteMutator' +export * from './Mutator/ItemMutator' +export * from './Types/AppDataField' +export * from './Types/AppDataField' +export * from './Types/ConflictStrategy' +export * from './Types/DefaultAppDomain' +export * from './Types/DefaultAppDomain' +export * from './Types/MutationType' +export * from './Types/SingletonStrategy' diff --git a/packages/models/src/Domain/Abstract/Payload/Implementations/DecryptedPayload.ts b/packages/models/src/Domain/Abstract/Payload/Implementations/DecryptedPayload.ts new file mode 100644 index 000000000..da52b7e96 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Implementations/DecryptedPayload.ts @@ -0,0 +1,71 @@ +import { Uuid } from '@standardnotes/common' +import { Copy } from '@standardnotes/utils' +import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload' +import { FillItemContent, ItemContent } from '../../Content/ItemContent' +import { ContentReference } from '../../Reference/ContentReference' +import { DecryptedTransferPayload } from '../../TransferPayload/Interfaces/DecryptedTransferPayload' +import { DecryptedPayloadInterface } from '../Interfaces/DecryptedPayload' +import { PayloadSource } from '../Types/PayloadSource' +import { PurePayload } from './PurePayload' + +export class DecryptedPayload< + C extends ItemContent = ItemContent, + T extends DecryptedTransferPayload = DecryptedTransferPayload, + > + extends PurePayload + implements DecryptedPayloadInterface +{ + override readonly content: C + override readonly deleted: false + + constructor(rawPayload: T, source = PayloadSource.Constructor) { + super(rawPayload, source) + + this.content = Copy(FillItemContent(rawPayload.content)) + this.deleted = false + } + + get references(): ContentReference[] { + return this.content.references || [] + } + + public getReference(uuid: Uuid): ContentReference { + const result = this.references.find((ref) => ref.uuid === uuid) + + if (!result) { + throw new Error('Reference not found') + } + + return result + } + + override ejected(): DecryptedTransferPayload { + return { + ...super.ejected(), + content: this.content, + deleted: this.deleted, + } + } + + copy(override?: Partial, source = this.source): this { + const result = new DecryptedPayload( + { + ...this.ejected(), + ...override, + }, + source, + ) + return result as this + } + + copyAsSyncResolved(override?: Partial & SyncResolvedParams, source = this.source): SyncResolvedPayload { + const result = new DecryptedPayload( + { + ...this.ejected(), + ...override, + }, + source, + ) + return result as SyncResolvedPayload + } +} diff --git a/packages/models/src/Domain/Abstract/Payload/Implementations/DeletedPayload.ts b/packages/models/src/Domain/Abstract/Payload/Implementations/DeletedPayload.ts new file mode 100644 index 000000000..9ba4e988a --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Implementations/DeletedPayload.ts @@ -0,0 +1,54 @@ +import { DeletedTransferPayload } from './../../TransferPayload/Interfaces/DeletedTransferPayload' +import { DeletedPayloadInterface } from '../Interfaces/DeletedPayload' +import { PayloadSource } from '../Types/PayloadSource' +import { PurePayload } from './PurePayload' +import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload' + +export class DeletedPayload extends PurePayload implements DeletedPayloadInterface { + override readonly deleted: true + override readonly content: undefined + + constructor(rawPayload: DeletedTransferPayload, source = PayloadSource.Constructor) { + super(rawPayload, source) + + this.deleted = true + this.content = undefined + } + + get discardable(): boolean | undefined { + return !this.dirty + } + + override ejected(): DeletedTransferPayload { + return { + ...super.ejected(), + deleted: this.deleted, + content: undefined, + } + } + + copy(override?: Partial, source = this.source): this { + const result = new DeletedPayload( + { + ...this.ejected(), + ...override, + }, + source, + ) + return result as this + } + + copyAsSyncResolved( + override?: Partial & SyncResolvedParams, + source = this.source, + ): SyncResolvedPayload { + const result = new DeletedPayload( + { + ...this.ejected(), + ...override, + }, + source, + ) + return result as SyncResolvedPayload + } +} diff --git a/packages/models/src/Domain/Abstract/Payload/Implementations/EncryptedPayload.ts b/packages/models/src/Domain/Abstract/Payload/Implementations/EncryptedPayload.ts new file mode 100644 index 000000000..1bf32260d --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Implementations/EncryptedPayload.ts @@ -0,0 +1,68 @@ +import { ProtocolVersion, protocolVersionFromEncryptedString } from '@standardnotes/common' +import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload' +import { EncryptedTransferPayload } from '../../TransferPayload/Interfaces/EncryptedTransferPayload' +import { EncryptedPayloadInterface } from '../Interfaces/EncryptedPayload' +import { PayloadSource } from '../Types/PayloadSource' +import { PurePayload } from './PurePayload' + +export class EncryptedPayload extends PurePayload implements EncryptedPayloadInterface { + override readonly content: string + override readonly deleted: false + readonly auth_hash?: string + readonly enc_item_key: string + readonly errorDecrypting: boolean + readonly items_key_id: string | undefined + readonly version: ProtocolVersion + readonly waitingForKey: boolean + + constructor(rawPayload: EncryptedTransferPayload, source = PayloadSource.Constructor) { + super(rawPayload, source) + + this.auth_hash = rawPayload.auth_hash + this.content = rawPayload.content + this.deleted = false + this.enc_item_key = rawPayload.enc_item_key + this.errorDecrypting = rawPayload.errorDecrypting + this.items_key_id = rawPayload.items_key_id + this.version = protocolVersionFromEncryptedString(this.content) + this.waitingForKey = rawPayload.waitingForKey + } + + override ejected(): EncryptedTransferPayload { + return { + ...super.ejected(), + enc_item_key: this.enc_item_key, + items_key_id: this.items_key_id, + auth_hash: this.auth_hash, + errorDecrypting: this.errorDecrypting, + waitingForKey: this.waitingForKey, + content: this.content, + deleted: this.deleted, + } + } + + copy(override?: Partial, source = this.source): this { + const result = new EncryptedPayload( + { + ...this.ejected(), + ...override, + }, + source, + ) + return result as this + } + + copyAsSyncResolved( + override?: Partial & SyncResolvedParams, + source = this.source, + ): SyncResolvedPayload { + const result = new EncryptedPayload( + { + ...this.ejected(), + ...override, + }, + source, + ) + return result as SyncResolvedPayload + } +} diff --git a/packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts b/packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts new file mode 100644 index 000000000..ccd5a6830 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts @@ -0,0 +1,104 @@ +import { ContentType } from '@standardnotes/common' +import { deepFreeze, useBoolean } from '@standardnotes/utils' +import { PayloadInterface } from '../Interfaces/PayloadInterface' +import { PayloadSource } from '../Types/PayloadSource' +import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload' +import { ItemContent } from '../../Content/ItemContent' +import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload' + +type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infer U + ? U extends Record + ? { [K in keyof U]: U[K][0] } + : never + : never + +export abstract class PurePayload, C extends ItemContent = ItemContent> + implements PayloadInterface +{ + readonly source: PayloadSource + readonly uuid: string + readonly content_type: ContentType + readonly deleted: boolean + readonly content: C | string | undefined + + readonly created_at: Date + readonly updated_at: Date + readonly created_at_timestamp: number + readonly updated_at_timestamp: number + readonly dirtyIndex?: number + readonly globalDirtyIndexAtLastSync?: number + readonly dirty?: boolean + + readonly lastSyncBegan?: Date + readonly lastSyncEnd?: Date + + readonly duplicate_of?: string + + constructor(rawPayload: T, source = PayloadSource.Constructor) { + this.source = source + this.uuid = rawPayload.uuid + + if (!this.uuid) { + throw Error( + `Attempting to construct payload with null uuid + Content type: ${rawPayload.content_type}`, + ) + } + + this.content = rawPayload.content + this.content_type = rawPayload.content_type + this.deleted = useBoolean(rawPayload.deleted, false) + this.dirty = rawPayload.dirty + this.duplicate_of = rawPayload.duplicate_of + + this.created_at = new Date(rawPayload.created_at || new Date()) + this.updated_at = new Date(rawPayload.updated_at || 0) + + this.created_at_timestamp = rawPayload.created_at_timestamp || 0 + this.updated_at_timestamp = rawPayload.updated_at_timestamp || 0 + + this.lastSyncBegan = rawPayload.lastSyncBegan ? new Date(rawPayload.lastSyncBegan) : undefined + this.lastSyncEnd = rawPayload.lastSyncEnd ? new Date(rawPayload.lastSyncEnd) : undefined + + this.dirtyIndex = rawPayload.dirtyIndex + this.globalDirtyIndexAtLastSync = rawPayload.globalDirtyIndexAtLastSync + + const timeToAllowSubclassesToFinishConstruction = 0 + setTimeout(() => { + deepFreeze(this) + }, timeToAllowSubclassesToFinishConstruction) + } + + ejected(): TransferPayload { + const comprehensive: RequiredKeepUndefined = { + uuid: this.uuid, + content: this.content, + deleted: this.deleted, + content_type: this.content_type, + created_at: this.created_at, + updated_at: this.updated_at, + created_at_timestamp: this.created_at_timestamp, + updated_at_timestamp: this.updated_at_timestamp, + dirty: this.dirty, + duplicate_of: this.duplicate_of, + dirtyIndex: this.dirtyIndex, + globalDirtyIndexAtLastSync: this.globalDirtyIndexAtLastSync, + lastSyncBegan: this.lastSyncBegan, + lastSyncEnd: this.lastSyncEnd, + } + + return comprehensive + } + + public get serverUpdatedAt(): Date { + return this.updated_at + } + + public get serverUpdatedAtTimestamp(): number { + return this.updated_at_timestamp + } + + abstract copy(override?: Partial, source?: PayloadSource): this + + abstract copyAsSyncResolved(override?: Partial & SyncResolvedParams, source?: PayloadSource): SyncResolvedPayload +} diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/DecryptedPayload.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/DecryptedPayload.ts new file mode 100644 index 000000000..28bde92c7 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/DecryptedPayload.ts @@ -0,0 +1,15 @@ +import { Uuid } from '@standardnotes/common' +import { DecryptedTransferPayload } from './../../TransferPayload/Interfaces/DecryptedTransferPayload' +import { ItemContent } from '../../Content/ItemContent' +import { ContentReference } from '../../Reference/ContentReference' +import { PayloadInterface } from './PayloadInterface' + +export interface DecryptedPayloadInterface + extends PayloadInterface { + readonly content: C + deleted: false + + ejected(): DecryptedTransferPayload + get references(): ContentReference[] + getReference(uuid: Uuid): ContentReference +} diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/DeletedPayload.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/DeletedPayload.ts new file mode 100644 index 000000000..60c228954 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/DeletedPayload.ts @@ -0,0 +1,15 @@ +import { DeletedTransferPayload } from '../../TransferPayload' +import { PayloadInterface } from './PayloadInterface' + +export interface DeletedPayloadInterface extends PayloadInterface { + readonly deleted: true + readonly content: undefined + + /** + * Whether a payload can be discarded and removed from storage. + * This value is true if a payload is marked as deleted and not dirty. + */ + discardable: boolean | undefined + + ejected(): DeletedTransferPayload +} diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts new file mode 100644 index 000000000..33c86621e --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts @@ -0,0 +1,18 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { EncryptedTransferPayload } from '../../TransferPayload/Interfaces/EncryptedTransferPayload' +import { PayloadInterface } from './PayloadInterface' + +export interface EncryptedPayloadInterface extends PayloadInterface { + readonly content: string + readonly deleted: false + readonly enc_item_key: string + readonly items_key_id: string | undefined + readonly errorDecrypting: boolean + readonly waitingForKey: boolean + readonly version: ProtocolVersion + + /** @deprecated */ + readonly auth_hash?: string + + ejected(): EncryptedTransferPayload +} diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts new file mode 100644 index 000000000..73bf6a4d0 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts @@ -0,0 +1,41 @@ +import { SyncResolvedParams, SyncResolvedPayload } from './../../../Runtime/Deltas/Utilities/SyncResolvedPayload' +import { ContentType, Uuid } from '@standardnotes/common' +import { ItemContent } from '../../Content/ItemContent' +import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload' +import { PayloadSource } from '../Types/PayloadSource' + +export interface PayloadInterface { + readonly source: PayloadSource + readonly uuid: Uuid + readonly content_type: ContentType + content: C | string | undefined + deleted: boolean + + /** updated_at is set by the server only, and not the client.*/ + readonly updated_at: Date + readonly created_at: Date + readonly created_at_timestamp: number + readonly updated_at_timestamp: number + get serverUpdatedAt(): Date + get serverUpdatedAtTimestamp(): number + + readonly dirtyIndex?: number + readonly globalDirtyIndexAtLastSync?: number + readonly dirty?: boolean + + readonly lastSyncBegan?: Date + readonly lastSyncEnd?: Date + + readonly duplicate_of?: Uuid + + /** + * "Ejected" means a payload for + * generic, non-contextual consumption, such as saving to a backup file or syncing + * with a server. + */ + ejected(): TransferPayload + + copy(override?: Partial, source?: PayloadSource): this + + copyAsSyncResolved(override?: Partial & SyncResolvedParams, source?: PayloadSource): SyncResolvedPayload +} diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/TypeCheck.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/TypeCheck.ts new file mode 100644 index 000000000..c2d941656 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/TypeCheck.ts @@ -0,0 +1,29 @@ +import { ItemContent } from '../../Content/ItemContent' +import { + isDecryptedTransferPayload, + isDeletedTransferPayload, + isEncryptedTransferPayload, + isErrorDecryptingTransferPayload, +} from '../../TransferPayload' +import { DecryptedPayloadInterface } from './DecryptedPayload' +import { DeletedPayloadInterface } from './DeletedPayload' +import { EncryptedPayloadInterface } from './EncryptedPayload' +import { PayloadInterface } from './PayloadInterface' + +export function isDecryptedPayload( + payload: PayloadInterface, +): payload is DecryptedPayloadInterface { + return isDecryptedTransferPayload(payload) +} + +export function isEncryptedPayload(payload: PayloadInterface): payload is EncryptedPayloadInterface { + return isEncryptedTransferPayload(payload) +} + +export function isDeletedPayload(payload: PayloadInterface): payload is DeletedPayloadInterface { + return isDeletedTransferPayload(payload) +} + +export function isErrorDecryptingPayload(payload: PayloadInterface): payload is EncryptedPayloadInterface { + return isErrorDecryptingTransferPayload(payload) +} diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/UnionTypes.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/UnionTypes.ts new file mode 100644 index 000000000..541e29ab8 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/UnionTypes.ts @@ -0,0 +1,11 @@ +import { ItemContent } from '../../Content/ItemContent' +import { DecryptedPayloadInterface } from './DecryptedPayload' +import { DeletedPayloadInterface } from './DeletedPayload' +import { EncryptedPayloadInterface } from './EncryptedPayload' + +export type FullyFormedPayloadInterface = + | DecryptedPayloadInterface + | EncryptedPayloadInterface + | DeletedPayloadInterface + +export type AnyNonDecryptedPayloadInterface = EncryptedPayloadInterface | DeletedPayloadInterface diff --git a/packages/models/src/Domain/Abstract/Payload/Types/EmitSource.ts b/packages/models/src/Domain/Abstract/Payload/Types/EmitSource.ts new file mode 100644 index 000000000..b0a6607d7 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Types/EmitSource.ts @@ -0,0 +1,43 @@ +export enum PayloadEmitSource { + /** When an observer registers to stream items, the items are pushed immediately to the observer */ + InitialObserverRegistrationPush = 1, + + /** + * Payload when a client modifies item property then maps it to update UI. + * This also indicates that the item was dirtied + */ + LocalChanged, + LocalInserted, + LocalDatabaseLoaded, + /** The payload returned by offline sync operation */ + OfflineSyncSaved, + LocalRetrieved, + + FileImport, + + ComponentRetrieved, + /** Payloads received from an external component with the intention of creating a new item */ + ComponentCreated, + + /** + * When the payloads are about to sync, they are emitted by the sync service with updated + * values of lastSyncBegan. Payloads emitted from this source indicate that these payloads + * have been saved to disk, and are about to be synced + */ + PreSyncSave, + + RemoteRetrieved, + RemoteSaved, +} + +/** + * Whether the changed payload represents only an internal change that shouldn't + * require a UI refresh + */ +export function isPayloadSourceInternalChange(source: PayloadEmitSource): boolean { + return [PayloadEmitSource.RemoteSaved, PayloadEmitSource.PreSyncSave].includes(source) +} + +export function isPayloadSourceRetrieved(source: PayloadEmitSource): boolean { + return [PayloadEmitSource.RemoteRetrieved, PayloadEmitSource.ComponentRetrieved].includes(source) +} diff --git a/packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts b/packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts new file mode 100644 index 000000000..4177073db --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts @@ -0,0 +1,13 @@ +export enum PayloadSource { + /** + * Payloads with a source of Constructor means that the payload was created + * in isolated space by the caller, and does not yet have any app-related affiliation. + */ + Constructor = 1, + + RemoteRetrieved, + + RemoteSaved, + + FileImport, +} diff --git a/packages/models/src/Domain/Abstract/Payload/Types/TimestampDefaults.ts b/packages/models/src/Domain/Abstract/Payload/Types/TimestampDefaults.ts new file mode 100644 index 000000000..52a4aaf5b --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/Types/TimestampDefaults.ts @@ -0,0 +1,8 @@ +export function PayloadTimestampDefaults() { + return { + updated_at: new Date(0), + created_at: new Date(), + updated_at_timestamp: 0, + created_at_timestamp: 0, + } +} diff --git a/packages/models/src/Domain/Abstract/Payload/index.ts b/packages/models/src/Domain/Abstract/Payload/index.ts new file mode 100644 index 000000000..50604221f --- /dev/null +++ b/packages/models/src/Domain/Abstract/Payload/index.ts @@ -0,0 +1,13 @@ +export * from './Implementations/PurePayload' +export * from './Implementations/DecryptedPayload' +export * from './Implementations/EncryptedPayload' +export * from './Implementations/DeletedPayload' +export * from './Interfaces/DecryptedPayload' +export * from './Interfaces/DeletedPayload' +export * from './Interfaces/EncryptedPayload' +export * from './Interfaces/PayloadInterface' +export * from './Interfaces/TypeCheck' +export * from './Interfaces/UnionTypes' +export * from './Types/PayloadSource' +export * from './Types/EmitSource' +export * from './Types/TimestampDefaults' diff --git a/packages/models/src/Domain/Abstract/Reference/AnonymousReference.ts b/packages/models/src/Domain/Abstract/Reference/AnonymousReference.ts new file mode 100644 index 000000000..962f4f52f --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/AnonymousReference.ts @@ -0,0 +1,8 @@ +import { ContentType } from '@standardnotes/common' +import { ContenteReferenceType } from './ContenteReferenceType' + +export interface AnonymousReference { + uuid: string + content_type: ContentType + reference_type: ContenteReferenceType +} diff --git a/packages/models/src/Domain/Abstract/Reference/ContentReference.ts b/packages/models/src/Domain/Abstract/Reference/ContentReference.ts new file mode 100644 index 000000000..40401e3d0 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/ContentReference.ts @@ -0,0 +1,4 @@ +import { LegacyAnonymousReference } from './LegacyAnonymousReference' +import { Reference } from './Reference' + +export type ContentReference = LegacyAnonymousReference | Reference diff --git a/packages/models/src/Domain/Abstract/Reference/ContenteReferenceType.ts b/packages/models/src/Domain/Abstract/Reference/ContenteReferenceType.ts new file mode 100644 index 000000000..801a777bc --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/ContenteReferenceType.ts @@ -0,0 +1,5 @@ +export enum ContenteReferenceType { + TagToParentTag = 'TagToParentTag', + FileToNote = 'FileToNote', + TagToFile = 'TagToFile', +} diff --git a/packages/models/src/Domain/Abstract/Reference/FileToNoteReference.ts b/packages/models/src/Domain/Abstract/Reference/FileToNoteReference.ts new file mode 100644 index 000000000..34385f55f --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/FileToNoteReference.ts @@ -0,0 +1,8 @@ +import { ContentType } from '@standardnotes/common' +import { AnonymousReference } from './AnonymousReference' +import { ContenteReferenceType } from './ContenteReferenceType' + +export interface FileToNoteReference extends AnonymousReference { + content_type: ContentType.Note + reference_type: ContenteReferenceType.FileToNote +} diff --git a/packages/models/src/Domain/Abstract/Reference/Functions.ts b/packages/models/src/Domain/Abstract/Reference/Functions.ts new file mode 100644 index 000000000..d5d8edd72 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/Functions.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ContentType } from '@standardnotes/common' +import { ItemInterface } from '../Item/Interfaces/ItemInterface' +import { ContenteReferenceType } from './ContenteReferenceType' +import { ContentReference } from './ContentReference' +import { LegacyAnonymousReference } from './LegacyAnonymousReference' +import { LegacyTagToNoteReference } from './LegacyTagToNoteReference' +import { Reference } from './Reference' +import { TagToParentTagReference } from './TagToParentTagReference' + +export const isLegacyAnonymousReference = (x: ContentReference): x is LegacyAnonymousReference => { + return (x as any).reference_type === undefined +} + +export const isReference = (x: ContentReference): x is Reference => { + return (x as any).reference_type !== undefined +} + +export const isLegacyTagToNoteReference = ( + x: LegacyAnonymousReference, + currentItem: ItemInterface, +): x is LegacyTagToNoteReference => { + const isReferenceToANote = x.content_type === ContentType.Note + const isReferenceFromATag = currentItem.content_type === ContentType.Tag + return isReferenceToANote && isReferenceFromATag +} + +export const isTagToParentTagReference = (x: ContentReference): x is TagToParentTagReference => { + return isReference(x) && x.reference_type === ContenteReferenceType.TagToParentTag +} diff --git a/packages/models/src/Domain/Abstract/Reference/LegacyAnonymousReference.ts b/packages/models/src/Domain/Abstract/Reference/LegacyAnonymousReference.ts new file mode 100644 index 000000000..5d445eac1 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/LegacyAnonymousReference.ts @@ -0,0 +1,4 @@ +export interface LegacyAnonymousReference { + uuid: string + content_type: string +} diff --git a/packages/models/src/Domain/Abstract/Reference/LegacyTagToNoteReference.ts b/packages/models/src/Domain/Abstract/Reference/LegacyTagToNoteReference.ts new file mode 100644 index 000000000..61a47284c --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/LegacyTagToNoteReference.ts @@ -0,0 +1,6 @@ +import { ContentType } from '@standardnotes/common' +import { LegacyAnonymousReference } from './LegacyAnonymousReference' + +export interface LegacyTagToNoteReference extends LegacyAnonymousReference { + content_type: ContentType.Note +} diff --git a/packages/models/src/Domain/Abstract/Reference/Reference.ts b/packages/models/src/Domain/Abstract/Reference/Reference.ts new file mode 100644 index 000000000..2dc74358e --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/Reference.ts @@ -0,0 +1,3 @@ +import { TagToParentTagReference } from './TagToParentTagReference' + +export type Reference = TagToParentTagReference diff --git a/packages/models/src/Domain/Abstract/Reference/TagToFileReference.ts b/packages/models/src/Domain/Abstract/Reference/TagToFileReference.ts new file mode 100644 index 000000000..9662f4c6a --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/TagToFileReference.ts @@ -0,0 +1,8 @@ +import { ContentType } from '@standardnotes/common' +import { AnonymousReference } from './AnonymousReference' +import { ContenteReferenceType } from './ContenteReferenceType' + +export interface TagToFileReference extends AnonymousReference { + content_type: ContentType.File + reference_type: ContenteReferenceType.TagToFile +} diff --git a/packages/models/src/Domain/Abstract/Reference/TagToParentTagReference.ts b/packages/models/src/Domain/Abstract/Reference/TagToParentTagReference.ts new file mode 100644 index 000000000..96be8f144 --- /dev/null +++ b/packages/models/src/Domain/Abstract/Reference/TagToParentTagReference.ts @@ -0,0 +1,8 @@ +import { ContentType } from '@standardnotes/common' +import { AnonymousReference } from './AnonymousReference' +import { ContenteReferenceType } from './ContenteReferenceType' + +export interface TagToParentTagReference extends AnonymousReference { + content_type: ContentType.Tag + reference_type: ContenteReferenceType.TagToParentTag +} diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DecryptedTransferPayload.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DecryptedTransferPayload.ts new file mode 100644 index 000000000..b850d9710 --- /dev/null +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DecryptedTransferPayload.ts @@ -0,0 +1,6 @@ +import { ItemContent } from '../../Content/ItemContent' +import { TransferPayload } from './TransferPayload' + +export interface DecryptedTransferPayload extends TransferPayload { + content: C +} diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DeletedTransferPayload.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DeletedTransferPayload.ts new file mode 100644 index 000000000..e2b3626dc --- /dev/null +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/DeletedTransferPayload.ts @@ -0,0 +1,6 @@ +import { TransferPayload } from './TransferPayload' + +export interface DeletedTransferPayload extends TransferPayload { + content: undefined + deleted: true +} diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/EncryptedTransferPayload.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/EncryptedTransferPayload.ts new file mode 100644 index 000000000..314b9e26d --- /dev/null +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/EncryptedTransferPayload.ts @@ -0,0 +1,11 @@ +import { TransferPayload } from './TransferPayload' + +export interface EncryptedTransferPayload extends TransferPayload { + content: string + enc_item_key: string + items_key_id: string | undefined + errorDecrypting: boolean + waitingForKey: boolean + /** @deprecated */ + auth_hash?: string +} diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts new file mode 100644 index 000000000..9ff995232 --- /dev/null +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts @@ -0,0 +1,23 @@ +import { ContentType, Uuid } from '@standardnotes/common' +import { ItemContent } from '../../Content/ItemContent' + +export interface TransferPayload { + uuid: Uuid + content_type: ContentType + content: C | string | undefined + deleted?: boolean + + updated_at: Date + created_at: Date + created_at_timestamp: number + updated_at_timestamp: number + + dirtyIndex?: number + globalDirtyIndexAtLastSync?: number + dirty?: boolean + + lastSyncBegan?: Date + lastSyncEnd?: Date + + duplicate_of?: Uuid +} diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts new file mode 100644 index 000000000..2c53b8789 --- /dev/null +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts @@ -0,0 +1,28 @@ +import { isObject, isString } from '@standardnotes/utils' +import { DecryptedTransferPayload } from './DecryptedTransferPayload' +import { DeletedTransferPayload } from './DeletedTransferPayload' +import { EncryptedTransferPayload } from './EncryptedTransferPayload' +import { TransferPayload } from './TransferPayload' + +export type FullyFormedTransferPayload = DecryptedTransferPayload | EncryptedTransferPayload | DeletedTransferPayload + +export function isDecryptedTransferPayload(payload: TransferPayload): payload is DecryptedTransferPayload { + return isObject(payload.content) +} + +export function isEncryptedTransferPayload(payload: TransferPayload): payload is EncryptedTransferPayload { + return 'content' in payload && isString(payload.content) +} + +export function isErrorDecryptingTransferPayload(payload: TransferPayload): payload is EncryptedTransferPayload { + return isEncryptedTransferPayload(payload) && payload.errorDecrypting === true +} + +export function isDeletedTransferPayload(payload: TransferPayload): payload is DeletedTransferPayload { + return 'deleted' in payload && payload.deleted === true +} + +export function isCorruptTransferPayload(payload: TransferPayload): boolean { + const invalidDeletedState = payload.deleted === true && payload.content != undefined + return payload.uuid == undefined || invalidDeletedState +} diff --git a/packages/models/src/Domain/Abstract/TransferPayload/index.ts b/packages/models/src/Domain/Abstract/TransferPayload/index.ts new file mode 100644 index 000000000..638d4db1b --- /dev/null +++ b/packages/models/src/Domain/Abstract/TransferPayload/index.ts @@ -0,0 +1,5 @@ +export * from './Interfaces/DecryptedTransferPayload' +export * from './Interfaces/DeletedTransferPayload' +export * from './Interfaces/EncryptedTransferPayload' +export * from './Interfaces/TransferPayload' +export * from './Interfaces/TypeCheck' diff --git a/packages/models/src/Domain/Local/KeyParams/RootKeyParamsInterface.ts b/packages/models/src/Domain/Local/KeyParams/RootKeyParamsInterface.ts new file mode 100644 index 000000000..03668ab59 --- /dev/null +++ b/packages/models/src/Domain/Local/KeyParams/RootKeyParamsInterface.ts @@ -0,0 +1,47 @@ +import { + KeyParamsContent001, + KeyParamsContent002, + KeyParamsContent003, + KeyParamsContent004, + AnyKeyParamsContent, + ProtocolVersion, + KeyParamsOrigination, +} from '@standardnotes/common' + +/** + * Key params are public data that contain information about how a root key was created. + * Given a keyParams object and a password, clients can compute a root key that was created + * previously. + */ +export interface RootKeyParamsInterface { + readonly content: AnyKeyParamsContent + + /** + * For consumers to determine whether the object they are + * working with is a proper RootKeyParams object. + */ + get isKeyParamsObject(): boolean + + get identifier(): string + + get version(): ProtocolVersion + get origination(): KeyParamsOrigination | undefined + + get content001(): KeyParamsContent001 + + get content002(): KeyParamsContent002 + + get content003(): KeyParamsContent003 + + get content004(): KeyParamsContent004 + + get createdDate(): Date | undefined + + compare(other: RootKeyParamsInterface): boolean + + /** + * When saving in a file or communicating with server, + * use the original values. + */ + getPortableValue(): AnyKeyParamsContent +} diff --git a/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts b/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts new file mode 100644 index 000000000..9e5a12bd7 --- /dev/null +++ b/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts @@ -0,0 +1,31 @@ +import { ApplicationIdentifier, ProtocolVersion } from '@standardnotes/common' +import { RootKeyContentSpecialized } from './RootKeyContent' + +export type RawKeychainValue = Record + +export interface NamespacedRootKeyInKeychain { + version: ProtocolVersion + masterKey: string + dataAuthenticationKey?: string +} + +export type RootKeyContentInStorage = RootKeyContentSpecialized + +export interface LegacyRawKeychainValue { + mk: string + ak: string + version: ProtocolVersion +} + +export type LegacyMobileKeychainStructure = { + offline?: { + timing?: unknown + pw?: string + } + encryptedAccountKeys?: unknown + mk: string + pw: string + ak: string + version?: string + jwt?: string +} diff --git a/packages/models/src/Domain/Local/RootKey/RootKeyContent.ts b/packages/models/src/Domain/Local/RootKey/RootKeyContent.ts new file mode 100644 index 000000000..f4f1c56c7 --- /dev/null +++ b/packages/models/src/Domain/Local/RootKey/RootKeyContent.ts @@ -0,0 +1,12 @@ +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { ProtocolVersion, AnyKeyParamsContent } from '@standardnotes/common' + +export interface RootKeyContentSpecialized { + version: ProtocolVersion + masterKey: string + serverPassword?: string + dataAuthenticationKey?: string + keyParams: AnyKeyParamsContent +} + +export type RootKeyContent = RootKeyContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts b/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts new file mode 100644 index 000000000..cae177525 --- /dev/null +++ b/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts @@ -0,0 +1,17 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { RootKeyParamsInterface } from '../KeyParams/RootKeyParamsInterface' +import { NamespacedRootKeyInKeychain, RootKeyContentInStorage } from './KeychainTypes' +import { RootKeyContent } from './RootKeyContent' + +export interface RootKeyInterface extends DecryptedItemInterface { + readonly keyParams: RootKeyParamsInterface + get keyVersion(): ProtocolVersion + get itemsKey(): string + get masterKey(): string + get serverPassword(): string | undefined + get dataAuthenticationKey(): string | undefined + compare(otherKey: RootKeyInterface): boolean + persistableValueWhenWrapping(): RootKeyContentInStorage + getKeychainValue(): NamespacedRootKeyInKeychain +} diff --git a/packages/models/src/Domain/Runtime/Collection/Collection.ts b/packages/models/src/Domain/Runtime/Collection/Collection.ts new file mode 100644 index 000000000..eb7f61ef3 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Collection.ts @@ -0,0 +1,263 @@ +import { extendArray, isObject, isString, UuidMap } from '@standardnotes/utils' +import { ContentType, Uuid } from '@standardnotes/common' +import { remove } from 'lodash' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { ContentReference } from '../../Abstract/Item' + +export interface CollectionElement { + uuid: Uuid + content_type: ContentType + dirty?: boolean + deleted?: boolean +} + +export interface DecryptedCollectionElement extends CollectionElement { + content: C + references: ContentReference[] +} + +export interface DeletedCollectionElement extends CollectionElement { + content: undefined + deleted: true +} + +export interface EncryptedCollectionElement extends CollectionElement { + content: string + errorDecrypting: boolean +} + +export abstract class Collection< + Element extends Decrypted | Encrypted | Deleted, + Decrypted extends DecryptedCollectionElement, + Encrypted extends EncryptedCollectionElement, + Deleted extends DeletedCollectionElement, +> { + readonly map: Partial> = {} + readonly typedMap: Partial> = {} + + /** An array of uuids of items that are dirty */ + dirtyIndex: Set = new Set() + + /** An array of uuids of items that are not marked as deleted */ + nondeletedIndex: Set = new Set() + + /** An array of uuids of items that are errorDecrypting or waitingForKey */ + invalidsIndex: Set = new Set() + + readonly referenceMap: UuidMap + + /** Maintains an index for each item uuid where the value is an array of uuids that are + * conflicts of that item. So if Note B and C are conflicts of Note A, + * conflictMap[A.uuid] == [B.uuid, C.uuid] */ + readonly conflictMap: UuidMap + + isDecryptedElement = (e: Decrypted | Encrypted | Deleted): e is Decrypted => { + return isObject(e.content) + } + + isEncryptedElement = (e: Decrypted | Encrypted | Deleted): e is Encrypted => { + return 'content' in e && isString(e.content) + } + + isErrorDecryptingElement = (e: Decrypted | Encrypted | Deleted): e is Encrypted => { + return this.isEncryptedElement(e) && e.errorDecrypting === true + } + + isDeletedElement = (e: Decrypted | Encrypted | Deleted): e is Deleted => { + return 'deleted' in e && e.deleted === true + } + + isNonDeletedElement = (e: Decrypted | Encrypted | Deleted): e is Decrypted | Encrypted => { + return !this.isDeletedElement(e) + } + + constructor( + copy = false, + mapCopy?: Partial>, + typedMapCopy?: Partial>, + referenceMapCopy?: UuidMap, + conflictMapCopy?: UuidMap, + ) { + if (copy) { + this.map = mapCopy! + this.typedMap = typedMapCopy! + this.referenceMap = referenceMapCopy! + this.conflictMap = conflictMapCopy! + } else { + this.referenceMap = new UuidMap() + this.conflictMap = new UuidMap() + } + } + + public uuids(): Uuid[] { + return Object.keys(this.map) + } + + public all(contentType?: ContentType | ContentType[]): Element[] { + if (contentType) { + if (Array.isArray(contentType)) { + const elements: Element[] = [] + for (const type of contentType) { + extendArray(elements, this.typedMap[type] || []) + } + return elements + } else { + return this.typedMap[contentType]?.slice() || [] + } + } else { + return Object.keys(this.map).map((uuid: Uuid) => { + return this.map[uuid] + }) as Element[] + } + } + + /** Returns all elements that are not marked as deleted */ + public nondeletedElements(): Element[] { + const uuids = Array.from(this.nondeletedIndex) + return this.findAll(uuids).filter(this.isNonDeletedElement) + } + + /** Returns all elements that are errorDecrypting or waitingForKey */ + public invalidElements(): Encrypted[] { + const uuids = Array.from(this.invalidsIndex) + return this.findAll(uuids) as Encrypted[] + } + + /** Returns all elements that are marked as dirty */ + public dirtyElements(): Element[] { + const uuids = Array.from(this.dirtyIndex) + return this.findAll(uuids) + } + + public findAll(uuids: Uuid[]): Element[] { + const results: Element[] = [] + + for (const id of uuids) { + const element = this.map[id] + if (element) { + results.push(element) + } + } + + return results + } + + public find(uuid: Uuid): Element | undefined { + return this.map[uuid] + } + + public has(uuid: Uuid): boolean { + return this.find(uuid) != undefined + } + + /** + * If an item is not found, an `undefined` element + * will be inserted into the array. + */ + public findAllIncludingBlanks(uuids: Uuid[]): (E | Deleted | undefined)[] { + const results: (E | Deleted | undefined)[] = [] + + for (const id of uuids) { + const element = this.map[id] as E | Deleted | undefined + results.push(element) + } + + return results + } + + public set(elements: Element | Element[]): void { + elements = Array.isArray(elements) ? elements : [elements] + + if (elements.length === 0) { + console.warn('Attempting to set 0 elements onto collection') + return + } + + for (const element of elements) { + this.map[element.uuid] = element + this.setToTypedMap(element) + + if (this.isErrorDecryptingElement(element)) { + this.invalidsIndex.add(element.uuid) + } else { + this.invalidsIndex.delete(element.uuid) + } + + if (this.isDecryptedElement(element)) { + const conflictOf = element.content.conflict_of + if (conflictOf) { + this.conflictMap.establishRelationship(conflictOf, element.uuid) + } + + this.referenceMap.setAllRelationships( + element.uuid, + element.references.map((r) => r.uuid), + ) + } + + if (element.dirty) { + this.dirtyIndex.add(element.uuid) + } else { + this.dirtyIndex.delete(element.uuid) + } + + if (element.deleted) { + this.nondeletedIndex.delete(element.uuid) + } else { + this.nondeletedIndex.add(element.uuid) + } + } + } + + public discard(elements: Element | Element[]): void { + elements = Array.isArray(elements) ? elements : [elements] + for (const element of elements) { + this.deleteFromTypedMap(element) + delete this.map[element.uuid] + this.conflictMap.removeFromMap(element.uuid) + this.referenceMap.removeFromMap(element.uuid) + } + } + + public uuidReferencesForUuid(uuid: Uuid): Uuid[] { + return this.referenceMap.getDirectRelationships(uuid) + } + + public uuidsThatReferenceUuid(uuid: Uuid): Uuid[] { + return this.referenceMap.getInverseRelationships(uuid) + } + + public referencesForElement(element: Decrypted): Element[] { + const uuids = this.referenceMap.getDirectRelationships(element.uuid) + return this.findAll(uuids) + } + + public conflictsOf(uuid: Uuid): Element[] { + const uuids = this.conflictMap.getDirectRelationships(uuid) + return this.findAll(uuids) + } + + public elementsReferencingElement(element: Decrypted, contentType?: ContentType): Element[] { + const uuids = this.uuidsThatReferenceUuid(element.uuid) + const items = this.findAll(uuids) + + if (!contentType) { + return items + } + + return items.filter((item) => item.content_type === contentType) + } + + private setToTypedMap(element: Element): void { + const array = this.typedMap[element.content_type] || [] + remove(array, { uuid: element.uuid as never }) + array.push(element) + this.typedMap[element.content_type] = array + } + + private deleteFromTypedMap(element: Element): void { + const array = this.typedMap[element.content_type] || [] + remove(array, { uuid: element.uuid as never }) + this.typedMap[element.content_type] = array + } +} diff --git a/packages/models/src/Domain/Runtime/Collection/CollectionInterface.ts b/packages/models/src/Domain/Runtime/Collection/CollectionInterface.ts new file mode 100644 index 000000000..8f025982e --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/CollectionInterface.ts @@ -0,0 +1,13 @@ +import { UuidMap } from '@standardnotes/utils' + +export interface CollectionInterface { + /** Maintains an index where the direct map for each item id is an array + * of item ids that the item references. This is essentially equivalent to + * item.content.references, but keeps state even when the item is deleted. + * So if tag A references Note B, referenceMap.directMap[A.uuid] == [B.uuid]. + * The inverse map for each item is an array of item ids where the items reference the + * key item. So if tag A references Note B, referenceMap.inverseMap[B.uuid] == [A.uuid]. + * This allows callers to determine for a given item, who references it? + * It would be prohibitive to look this up on demand */ + readonly referenceMap: UuidMap +} diff --git a/packages/models/src/Domain/Runtime/Collection/CollectionSort.ts b/packages/models/src/Domain/Runtime/Collection/CollectionSort.ts new file mode 100644 index 000000000..166012dcc --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/CollectionSort.ts @@ -0,0 +1,20 @@ +import { Uuid, ContentType } from '@standardnotes/common' + +export interface SortableItem { + uuid: Uuid + content_type: ContentType + created_at: Date + userModifiedDate: Date + title?: string + pinned: boolean +} + +export const CollectionSort: Record = { + CreatedAt: 'created_at', + UpdatedAt: 'userModifiedDate', + Title: 'title', +} + +export type CollectionSortDirection = 'asc' | 'dsc' + +export type CollectionSortProperty = keyof SortableItem diff --git a/packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.spec.ts b/packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.spec.ts new file mode 100644 index 000000000..ae84e33d5 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.spec.ts @@ -0,0 +1,36 @@ +import { NoteContent } from './../../../Syncable/Note/NoteContent' +import { ContentType } from '@standardnotes/common' +import { DecryptedItem } from '../../../Abstract/Item' +import { DecryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload' +import { ItemCollection } from './ItemCollection' +import { FillItemContent, ItemContent } from '../../../Abstract/Content/ItemContent' + +describe('item collection', () => { + const createDecryptedPayload = (uuid?: string): DecryptedPayload => { + return new DecryptedPayload({ + uuid: uuid || String(Math.random()), + content_type: ContentType.Note, + content: FillItemContent({ + title: 'foo', + }), + ...PayloadTimestampDefaults(), + }) + } + + it('setting same item twice should not result in doubles', () => { + const collection = new ItemCollection() + + const decryptedItem = new DecryptedItem(createDecryptedPayload()) + collection.set(decryptedItem) + + const updatedItem = new DecryptedItem( + decryptedItem.payload.copy({ + content: { foo: 'bar' } as unknown as jest.Mocked, + }), + ) + + collection.set(updatedItem) + + expect(collection.all()).toHaveLength(1) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.ts b/packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.ts new file mode 100644 index 000000000..8a553d3d4 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Item/ItemCollection.ts @@ -0,0 +1,59 @@ +import { ItemContent } from './../../../Abstract/Content/ItemContent' +import { EncryptedItemInterface } from './../../../Abstract/Item/Interfaces/EncryptedItem' +import { ContentType, Uuid } from '@standardnotes/common' +import { SNIndex } from '../../Index/SNIndex' +import { isDecryptedItem } from '../../../Abstract/Item/Interfaces/TypeCheck' +import { DecryptedItemInterface } from '../../../Abstract/Item/Interfaces/DecryptedItem' +import { CollectionInterface } from '../CollectionInterface' +import { DeletedItemInterface } from '../../../Abstract/Item' +import { Collection } from '../Collection' +import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes' +import { ItemDelta } from '../../Index/ItemDelta' + +export class ItemCollection + extends Collection + implements SNIndex, CollectionInterface +{ + public onChange(delta: ItemDelta): void { + const changedOrInserted = delta.changed.concat(delta.inserted) + + if (changedOrInserted.length > 0) { + this.set(changedOrInserted) + } + + this.discard(delta.discarded) + } + + public findDecrypted(uuid: Uuid): T | undefined { + const result = this.find(uuid) + + if (!result) { + return undefined + } + + return isDecryptedItem(result) ? (result as T) : undefined + } + + public findAllDecrypted(uuids: Uuid[]): T[] { + return this.findAll(uuids).filter(isDecryptedItem) as T[] + } + + public findAllDecryptedWithBlanks( + uuids: Uuid[], + ): (DecryptedItemInterface | undefined)[] { + const results = this.findAllIncludingBlanks(uuids) + const mapped = results.map((i) => { + if (i == undefined || isDecryptedItem(i)) { + return i + } + + return undefined + }) + + return mapped as (DecryptedItemInterface | undefined)[] + } + + public allDecrypted(contentType: ContentType | ContentType[]): T[] { + return this.all(contentType).filter(isDecryptedItem) as T[] + } +} diff --git a/packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.spec.ts b/packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.spec.ts new file mode 100644 index 000000000..643b96135 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.spec.ts @@ -0,0 +1,65 @@ +import { NoteContent } from './../../../Syncable/Note/NoteContent' +import { ContentType } from '@standardnotes/common' +import { DecryptedItem, EncryptedItem } from '../../../Abstract/Item' +import { DecryptedPayload, EncryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload' +import { ItemCollection } from './ItemCollection' +import { FillItemContent } from '../../../Abstract/Content/ItemContent' +import { TagNotesIndex } from './TagNotesIndex' +import { ItemDelta } from '../../Index/ItemDelta' +import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes' + +describe('tag notes index', () => { + const createEncryptedItem = (uuid?: string) => { + const payload = new EncryptedPayload({ + uuid: uuid || String(Math.random()), + content_type: ContentType.Note, + content: '004:...', + enc_item_key: '004:...', + items_key_id: '123', + waitingForKey: true, + errorDecrypting: true, + ...PayloadTimestampDefaults(), + }) + + return new EncryptedItem(payload) + } + + const createDecryptedItem = (uuid?: string) => { + const payload = new DecryptedPayload({ + uuid: uuid || String(Math.random()), + content_type: ContentType.Note, + content: FillItemContent({ + title: 'foo', + }), + ...PayloadTimestampDefaults(), + }) + return new DecryptedItem(payload) + } + + const createChangeDelta = (item: AnyItemInterface): ItemDelta => { + return { + changed: [item], + inserted: [], + discarded: [], + ignored: [], + unerrored: [], + } + } + + it('should decrement count after decrypted note becomes errored', () => { + const collection = new ItemCollection() + const index = new TagNotesIndex(collection) + + const decryptedItem = createDecryptedItem() + collection.set(decryptedItem) + index.onChange(createChangeDelta(decryptedItem)) + + expect(index.allCountableNotesCount()).toEqual(1) + + const encryptedItem = createEncryptedItem(decryptedItem.uuid) + collection.set(encryptedItem) + index.onChange(createChangeDelta(encryptedItem)) + + expect(index.allCountableNotesCount()).toEqual(0) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.ts b/packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.ts new file mode 100644 index 000000000..af2007286 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Item/TagNotesIndex.ts @@ -0,0 +1,111 @@ +import { removeFromArray } from '@standardnotes/utils' +import { ContentType, Uuid } from '@standardnotes/common' +import { isTag, SNTag } from '../../../Syncable/Tag/Tag' +import { SNIndex } from '../../Index/SNIndex' +import { ItemCollection } from './ItemCollection' +import { ItemDelta } from '../../Index/ItemDelta' +import { isDecryptedItem, ItemInterface } from '../../../Abstract/Item' + +type AllNotesUuidSignifier = undefined +export type TagNoteCountChangeObserver = (tagUuid: Uuid | AllNotesUuidSignifier) => void + +export class TagNotesIndex implements SNIndex { + private tagToNotesMap: Partial>> = {} + private allCountableNotes = new Set() + + constructor(private collection: ItemCollection, public observers: TagNoteCountChangeObserver[] = []) {} + + private isNoteCountable = (note: ItemInterface) => { + if (isDecryptedItem(note)) { + return !note.archived && !note.trashed + } + return false + } + + public addCountChangeObserver(observer: TagNoteCountChangeObserver): () => void { + this.observers.push(observer) + + const thislessEventObservers = this.observers + return () => { + removeFromArray(thislessEventObservers, observer) + } + } + + private notifyObservers(tagUuid: Uuid | undefined) { + for (const observer of this.observers) { + observer(tagUuid) + } + } + + public allCountableNotesCount(): number { + return this.allCountableNotes.size + } + + public countableNotesForTag(tag: SNTag): number { + return this.tagToNotesMap[tag.uuid]?.size || 0 + } + + public onChange(delta: ItemDelta): void { + const notes = [...delta.changed, ...delta.inserted, ...delta.discarded].filter( + (i) => i.content_type === ContentType.Note, + ) + const tags = [...delta.changed, ...delta.inserted].filter(isDecryptedItem).filter(isTag) + + this.receiveNoteChanges(notes) + this.receiveTagChanges(tags) + } + + private receiveTagChanges(tags: SNTag[]): void { + for (const tag of tags) { + const uuids = tag.noteReferences.map((ref) => ref.uuid) + const countableUuids = uuids.filter((uuid) => this.allCountableNotes.has(uuid)) + const previousSet = this.tagToNotesMap[tag.uuid] + this.tagToNotesMap[tag.uuid] = new Set(countableUuids) + + if (previousSet?.size !== countableUuids.length) { + this.notifyObservers(tag.uuid) + } + } + } + + private receiveNoteChanges(notes: ItemInterface[]): void { + const previousAllCount = this.allCountableNotes.size + + for (const note of notes) { + const isCountable = this.isNoteCountable(note) + if (isCountable) { + this.allCountableNotes.add(note.uuid) + } else { + this.allCountableNotes.delete(note.uuid) + } + + const associatedTagUuids = this.collection.uuidsThatReferenceUuid(note.uuid) + + for (const tagUuid of associatedTagUuids) { + const set = this.setForTag(tagUuid) + const previousCount = set.size + if (isCountable) { + set.add(note.uuid) + } else { + set.delete(note.uuid) + } + if (previousCount !== set.size) { + this.notifyObservers(tagUuid) + } + } + } + + if (previousAllCount !== this.allCountableNotes.size) { + this.notifyObservers(undefined) + } + } + + private setForTag(uuid: Uuid): Set { + let set = this.tagToNotesMap[uuid] + if (!set) { + set = new Set() + this.tagToNotesMap[uuid] = set + } + return set + } +} diff --git a/packages/models/src/Domain/Runtime/Collection/Payload/ImmutablePayloadCollection.ts b/packages/models/src/Domain/Runtime/Collection/Payload/ImmutablePayloadCollection.ts new file mode 100644 index 000000000..3d52bfe8d --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Payload/ImmutablePayloadCollection.ts @@ -0,0 +1,54 @@ +import { FullyFormedPayloadInterface } from './../../../Abstract/Payload/Interfaces/UnionTypes' +import { ContentType } from '@standardnotes/common' +import { UuidMap } from '@standardnotes/utils' +import { PayloadCollection } from './PayloadCollection' + +export class ImmutablePayloadCollection< + P extends FullyFormedPayloadInterface = FullyFormedPayloadInterface, +> extends PayloadCollection

{ + public get payloads(): P[] { + return this.all() + } + + /** We don't use a constructor for this because we don't want the constructor to have + * side-effects, such as calling collection.set(). */ + static WithPayloads(payloads: T[] = []): ImmutablePayloadCollection { + const collection = new ImmutablePayloadCollection() + if (payloads.length > 0) { + collection.set(payloads) + } + + Object.freeze(collection) + return collection + } + + static FromCollection( + collection: PayloadCollection, + ): ImmutablePayloadCollection { + const mapCopy = Object.freeze(Object.assign({}, collection.map)) + const typedMapCopy = Object.freeze(Object.assign({}, collection.typedMap)) + const referenceMapCopy = Object.freeze(collection.referenceMap.makeCopy()) as UuidMap + const conflictMapCopy = Object.freeze(collection.conflictMap.makeCopy()) as UuidMap + + const result = new ImmutablePayloadCollection( + true, + mapCopy, + typedMapCopy as Partial>, + referenceMapCopy, + conflictMapCopy, + ) + + Object.freeze(result) + + return result + } + + mutableCopy(): PayloadCollection

{ + const mapCopy = Object.assign({}, this.map) + const typedMapCopy = Object.assign({}, this.typedMap) + const referenceMapCopy = this.referenceMap.makeCopy() + const conflictMapCopy = this.conflictMap.makeCopy() + const result = new PayloadCollection(true, mapCopy, typedMapCopy, referenceMapCopy, conflictMapCopy) + return result + } +} diff --git a/packages/models/src/Domain/Runtime/Collection/Payload/PayloadCollection.ts b/packages/models/src/Domain/Runtime/Collection/Payload/PayloadCollection.ts new file mode 100644 index 000000000..1736b9e36 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Payload/PayloadCollection.ts @@ -0,0 +1,21 @@ +import { FullyFormedPayloadInterface } from './../../../Abstract/Payload/Interfaces/UnionTypes' +import { EncryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/EncryptedPayload' +import { CollectionInterface } from '../CollectionInterface' +import { DecryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/DecryptedPayload' +import { IntegrityPayload } from '@standardnotes/responses' +import { Collection } from '../Collection' +import { DeletedPayloadInterface } from '../../../Abstract/Payload' + +export class PayloadCollection

+ extends Collection + implements CollectionInterface +{ + public integrityPayloads(): IntegrityPayload[] { + const nondeletedElements = this.nondeletedElements() + + return nondeletedElements.map((item) => ({ + uuid: item.uuid, + updated_at_timestamp: item.serverUpdatedAtTimestamp as number, + })) + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaEmit.ts b/packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaEmit.ts new file mode 100644 index 000000000..318759e26 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaEmit.ts @@ -0,0 +1,30 @@ +import { extendArray } from '@standardnotes/utils' +import { EncryptedPayloadInterface, FullyFormedPayloadInterface, PayloadEmitSource } from '../../../Abstract/Payload' +import { SyncResolvedPayload } from '../Utilities/SyncResolvedPayload' + +export type DeltaEmit

= { + emits: P[] + ignored?: EncryptedPayloadInterface[] + source: PayloadEmitSource +} + +export type SyncDeltaEmit = { + emits: SyncResolvedPayload[] + ignored?: EncryptedPayloadInterface[] + source: PayloadEmitSource +} + +export type SourcelessSyncDeltaEmit = { + emits: SyncResolvedPayload[] + ignored: EncryptedPayloadInterface[] +} + +export function extendSyncDelta(base: SyncDeltaEmit, extendWith: SourcelessSyncDeltaEmit): void { + extendArray(base.emits, extendWith.emits) + if (extendWith.ignored) { + if (!base.ignored) { + base.ignored = [] + } + extendArray(base.ignored, extendWith.ignored) + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaInterface.ts b/packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaInterface.ts new file mode 100644 index 000000000..7f29b5510 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/Abstract/DeltaInterface.ts @@ -0,0 +1,24 @@ +import { ImmutablePayloadCollection } from '../../Collection/Payload/ImmutablePayloadCollection' +import { DeltaEmit } from './DeltaEmit' + +/** + * A payload delta is a class that defines instructions that process an incoming collection + * of payloads, applies some set of operations on those payloads wrt to the current base state, + * and returns the resulting collection. Deltas are purely functional and do not modify + * input data, instead returning what the collection would look like after its been + * transformed. The consumer may choose to act as they wish with this end result. + * + * A delta object takes a baseCollection (the current state of the data) and an applyCollection + * (the data another source is attempting to merge on top of our base data). The delta will + * then iterate over this data and return a `resultingCollection` object that includes the final + * state of the data after the class-specific operations have been applied. + * + * For example, the RemoteRetrieved delta will take the current state of local data as + * baseCollection, the data the server is sending as applyCollection, and determine what + * the end state of the data should look like. + */ +export interface DeltaInterface { + baseCollection: ImmutablePayloadCollection + + result(): DeltaEmit +} diff --git a/packages/models/src/Domain/Runtime/Deltas/Abstract/SyncDeltaInterface.ts b/packages/models/src/Domain/Runtime/Deltas/Abstract/SyncDeltaInterface.ts new file mode 100644 index 000000000..b73b23f08 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/Abstract/SyncDeltaInterface.ts @@ -0,0 +1,8 @@ +import { ImmutablePayloadCollection } from '../../Collection/Payload/ImmutablePayloadCollection' +import { SyncDeltaEmit } from './DeltaEmit' + +export interface SyncDeltaInterface { + baseCollection: ImmutablePayloadCollection + + result(): SyncDeltaEmit +} diff --git a/packages/models/src/Domain/Runtime/Deltas/Conflict.spec.ts b/packages/models/src/Domain/Runtime/Deltas/Conflict.spec.ts new file mode 100644 index 000000000..63c97883f --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/Conflict.spec.ts @@ -0,0 +1,102 @@ +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { ConflictStrategy } from '../../Abstract/Item' +import { + DecryptedPayload, + EncryptedPayload, + FullyFormedPayloadInterface, + PayloadTimestampDefaults, +} from '../../Abstract/Payload' +import { ItemsKeyContent } from '../../Syncable/ItemsKey/ItemsKeyInterface' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { PayloadCollection } from '../Collection/Payload/PayloadCollection' +import { HistoryMap } from '../History' +import { ConflictDelta } from './Conflict' + +describe('conflict delta', () => { + const historyMap = {} as HistoryMap + + const createBaseCollection = (payload: FullyFormedPayloadInterface) => { + const baseCollection = new PayloadCollection() + baseCollection.set(payload) + return ImmutablePayloadCollection.FromCollection(baseCollection) + } + + const createDecryptedItemsKey = (uuid: string, key: string, timestamp = 0) => { + return new DecryptedPayload({ + uuid: uuid, + content_type: ContentType.ItemsKey, + content: FillItemContent({ + itemsKey: key, + }), + ...PayloadTimestampDefaults(), + updated_at_timestamp: timestamp, + }) + } + + const createErroredItemsKey = (uuid: string, timestamp = 0) => { + return new EncryptedPayload({ + uuid: uuid, + content_type: ContentType.ItemsKey, + content: '004:...', + enc_item_key: '004:...', + items_key_id: undefined, + errorDecrypting: true, + waitingForKey: false, + ...PayloadTimestampDefaults(), + updated_at_timestamp: timestamp, + }) + } + + it('when apply is an items key, logic should be diverted to items key delta', () => { + const basePayload = createDecryptedItemsKey('123', 'secret') + + const baseCollection = createBaseCollection(basePayload) + + const applyPayload = createDecryptedItemsKey('123', 'secret', 2) + + const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap) + + const mocked = (delta.getConflictStrategy = jest.fn()) + + delta.result() + + expect(mocked).toBeCalledTimes(0) + }) + + it('if apply payload is errored but base payload is not, should duplicate base and keep apply', () => { + const basePayload = createDecryptedItemsKey('123', 'secret') + + const baseCollection = createBaseCollection(basePayload) + + const applyPayload = createErroredItemsKey('123', 2) + + const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap) + + expect(delta.getConflictStrategy()).toBe(ConflictStrategy.DuplicateBaseKeepApply) + }) + + it('if base payload is errored but apply is not, should keep base duplicate apply', () => { + const basePayload = createErroredItemsKey('123', 2) + + const baseCollection = createBaseCollection(basePayload) + + const applyPayload = createDecryptedItemsKey('123', 'secret') + + const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap) + + expect(delta.getConflictStrategy()).toBe(ConflictStrategy.KeepBaseDuplicateApply) + }) + + it('if base and apply are errored, should keep apply', () => { + const basePayload = createErroredItemsKey('123', 2) + + const baseCollection = createBaseCollection(basePayload) + + const applyPayload = createErroredItemsKey('123', 3) + + const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap) + + expect(delta.getConflictStrategy()).toBe(ConflictStrategy.KeepApply) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Deltas/Conflict.ts b/packages/models/src/Domain/Runtime/Deltas/Conflict.ts new file mode 100644 index 000000000..07f8fb101 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/Conflict.ts @@ -0,0 +1,225 @@ +import { greaterOfTwoDates, uniqCombineObjArrays } from '@standardnotes/utils' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { CreateDecryptedItemFromPayload, CreateItemFromPayload } from '../../Utilities/Item/ItemGenerator' +import { HistoryMap, historyMapFunctions } from '../History/HistoryMap' +import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy' +import { PayloadsByDuplicating } from '../../Utilities/Payload/PayloadsByDuplicating' +import { PayloadContentsEqual } from '../../Utilities/Payload/PayloadContentsEqual' +import { FullyFormedPayloadInterface } from '../../Abstract/Payload' +import { + isDecryptedPayload, + isErrorDecryptingPayload, + isDeletedPayload, +} from '../../Abstract/Payload/Interfaces/TypeCheck' +import { ContentType } from '@standardnotes/common' +import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' +import { ItemsKeyDelta } from './ItemsKeyDelta' +import { SourcelessSyncDeltaEmit } from './Abstract/DeltaEmit' +import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter' + +export class ConflictDelta { + constructor( + protected readonly baseCollection: ImmutablePayloadCollection, + protected readonly basePayload: FullyFormedPayloadInterface, + protected readonly applyPayload: FullyFormedPayloadInterface, + protected readonly historyMap: HistoryMap, + ) {} + + public result(): SourcelessSyncDeltaEmit { + if (this.applyPayload.content_type === ContentType.ItemsKey) { + const keyDelta = new ItemsKeyDelta(this.baseCollection, [this.applyPayload]) + + return keyDelta.result() + } + + const strategy = this.getConflictStrategy() + + return { + emits: this.handleStrategy(strategy), + ignored: [], + } + } + + getConflictStrategy(): ConflictStrategy { + const isBaseErrored = isErrorDecryptingPayload(this.basePayload) + const isApplyErrored = isErrorDecryptingPayload(this.applyPayload) + if (isBaseErrored || isApplyErrored) { + if (isBaseErrored && !isApplyErrored) { + return ConflictStrategy.KeepBaseDuplicateApply + } else if (!isBaseErrored && isApplyErrored) { + return ConflictStrategy.DuplicateBaseKeepApply + } else if (isBaseErrored && isApplyErrored) { + return ConflictStrategy.KeepApply + } + } else if (isDecryptedPayload(this.basePayload)) { + /** + * Ensure no conflict has already been created with the incoming content. + * This can occur in a multi-page sync request where in the middle of the request, + * we make changes to many items, including duplicating, but since we are still not + * uploading the changes until after the multi-page request completes, we may have + * already conflicted this item. + */ + const existingConflict = this.baseCollection.conflictsOf(this.applyPayload.uuid)[0] + if ( + existingConflict && + isDecryptedPayload(existingConflict) && + isDecryptedPayload(this.applyPayload) && + PayloadContentsEqual(existingConflict, this.applyPayload) + ) { + /** Conflict exists and its contents are the same as incoming value, do not make duplicate */ + return ConflictStrategy.KeepBase + } else { + const tmpBaseItem = CreateDecryptedItemFromPayload(this.basePayload) + const tmpApplyItem = CreateItemFromPayload(this.applyPayload) + const historyEntries = this.historyMap[this.basePayload.uuid] || [] + const previousRevision = historyMapFunctions.getNewestRevision(historyEntries) + + return tmpBaseItem.strategyWhenConflictingWithItem(tmpApplyItem, previousRevision) + } + } else if (isDeletedPayload(this.basePayload) || isDeletedPayload(this.applyPayload)) { + const baseDeleted = isDeletedPayload(this.basePayload) + const applyDeleted = isDeletedPayload(this.applyPayload) + if (baseDeleted && applyDeleted) { + return ConflictStrategy.KeepApply + } else { + return ConflictStrategy.KeepApply + } + } + + throw Error('Unhandled strategy in Conflict Delta getConflictStrategy') + } + + private handleStrategy(strategy: ConflictStrategy): SyncResolvedPayload[] { + if (strategy === ConflictStrategy.KeepBase) { + return this.handleKeepBaseStrategy() + } + + if (strategy === ConflictStrategy.KeepApply) { + return this.handleKeepApplyStrategy() + } + + if (strategy === ConflictStrategy.KeepBaseDuplicateApply) { + return this.handleKeepBaseDuplicateApplyStrategy() + } + + if (strategy === ConflictStrategy.DuplicateBaseKeepApply) { + return this.handleDuplicateBaseKeepApply() + } + + if (strategy === ConflictStrategy.KeepBaseMergeRefs) { + return this.handleKeepBaseMergeRefsStrategy() + } + + throw Error('Unhandled strategy in conflict delta payloadsByHandlingStrategy') + } + + private handleKeepBaseStrategy(): SyncResolvedPayload[] { + const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt) + + const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp) + + const leftPayload = this.basePayload.copyAsSyncResolved( + { + updated_at: updatedAt, + updated_at_timestamp: updatedAtTimestamp, + dirtyIndex: getIncrementedDirtyIndex(), + dirty: true, + lastSyncEnd: new Date(), + }, + this.applyPayload.source, + ) + + return [leftPayload] + } + + private handleKeepApplyStrategy(): SyncResolvedPayload[] { + const result = this.applyPayload.copyAsSyncResolved( + { + lastSyncBegan: this.basePayload.lastSyncBegan, + lastSyncEnd: new Date(), + dirty: false, + }, + this.applyPayload.source, + ) + + return [result] + } + + private handleKeepBaseDuplicateApplyStrategy(): SyncResolvedPayload[] { + const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt) + + const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp) + + const leftPayload = this.basePayload.copyAsSyncResolved( + { + updated_at: updatedAt, + updated_at_timestamp: updatedAtTimestamp, + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + lastSyncEnd: new Date(), + }, + this.applyPayload.source, + ) + + const rightPayloads = PayloadsByDuplicating({ + payload: this.applyPayload, + baseCollection: this.baseCollection, + isConflict: true, + source: this.applyPayload.source, + }) + + return [leftPayload].concat(rightPayloads) + } + + private handleDuplicateBaseKeepApply(): SyncResolvedPayload[] { + const leftPayloads = PayloadsByDuplicating({ + payload: this.basePayload, + baseCollection: this.baseCollection, + isConflict: true, + source: this.applyPayload.source, + }) + + const rightPayload = this.applyPayload.copyAsSyncResolved( + { + lastSyncBegan: this.basePayload.lastSyncBegan, + dirty: false, + lastSyncEnd: new Date(), + }, + this.applyPayload.source, + ) + + return leftPayloads.concat([rightPayload]) + } + + private handleKeepBaseMergeRefsStrategy(): SyncResolvedPayload[] { + if (!isDecryptedPayload(this.basePayload) || !isDecryptedPayload(this.applyPayload)) { + return [] + } + + const refs = uniqCombineObjArrays(this.basePayload.content.references, this.applyPayload.content.references, [ + 'uuid', + 'content_type', + ]) + + const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt) + + const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp) + + const payload = this.basePayload.copyAsSyncResolved( + { + updated_at: updatedAt, + updated_at_timestamp: updatedAtTimestamp, + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + lastSyncEnd: new Date(), + content: { + ...this.basePayload.content, + references: refs, + }, + }, + this.applyPayload.source, + ) + + return [payload] + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/FileImport.ts b/packages/models/src/Domain/Runtime/Deltas/FileImport.ts new file mode 100644 index 000000000..042ea0cb7 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/FileImport.ts @@ -0,0 +1,90 @@ +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { ConflictDelta } from './Conflict' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { DeletedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload' +import { HistoryMap } from '../History' +import { extendSyncDelta, SourcelessSyncDeltaEmit, SyncDeltaEmit } from './Abstract/DeltaEmit' +import { DeltaInterface } from './Abstract/DeltaInterface' +import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' +import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter' + +export class DeltaFileImport implements DeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + private readonly applyPayloads: DecryptedPayloadInterface[], + protected readonly historyMap: HistoryMap, + ) {} + + public result(): SyncDeltaEmit { + const result: SyncDeltaEmit = { + emits: [], + ignored: [], + source: PayloadEmitSource.FileImport, + } + + for (const payload of this.applyPayloads) { + const resolved = this.resolvePayload(payload, result) + + extendSyncDelta(result, resolved) + } + + return result + } + + private resolvePayload( + payload: DecryptedPayloadInterface | DeletedPayloadInterface, + currentResults: SyncDeltaEmit, + ): SourcelessSyncDeltaEmit { + /** + * Check to see if we've already processed a payload for this id. + * If so, that would be the latest value, and not what's in the base collection. + */ + + /* + * Find the most recently created conflict if available, as that + * would contain the most recent value. + */ + let current = currentResults.emits.find((candidate) => { + return isDecryptedPayload(candidate) && candidate.content.conflict_of === payload.uuid + }) + + /** + * If no latest conflict, find by uuid directly. + */ + if (!current) { + current = currentResults.emits.find((candidate) => { + return candidate.uuid === payload.uuid + }) + } + + /** + * If not found in current results, use the base value. + */ + if (!current) { + const base = this.baseCollection.find(payload.uuid) + if (base && isDecryptedPayload(base)) { + current = base as SyncResolvedPayload + } + } + + /** + * If the current doesn't exist, we're creating a new item from payload. + */ + if (!current) { + return { + emits: [ + payload.copyAsSyncResolved({ + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + lastSyncEnd: new Date(0), + }), + ], + ignored: [], + } + } + + const delta = new ConflictDelta(this.baseCollection, current, payload, this.historyMap) + + return delta.result() + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.spec.ts b/packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.spec.ts new file mode 100644 index 000000000..3ec99b697 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.spec.ts @@ -0,0 +1,54 @@ +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { + DecryptedPayload, + EncryptedPayload, + isEncryptedPayload, + PayloadTimestampDefaults, +} from '../../Abstract/Payload' +import { PayloadCollection } from '../Collection/Payload/PayloadCollection' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { ItemsKeyContent } from '../../Syncable/ItemsKey/ItemsKeyInterface' +import { ItemsKeyDelta } from './ItemsKeyDelta' + +describe('items key delta', () => { + it('if local items key is decrypted, incoming encrypted should not overwrite', async () => { + const baseCollection = new PayloadCollection() + const basePayload = new DecryptedPayload({ + uuid: '123', + content_type: ContentType.ItemsKey, + content: FillItemContent({ + itemsKey: 'secret', + }), + ...PayloadTimestampDefaults(), + updated_at_timestamp: 1, + }) + + baseCollection.set(basePayload) + + const payloadToIgnore = new EncryptedPayload({ + uuid: '123', + content_type: ContentType.ItemsKey, + content: '004:...', + enc_item_key: '004:...', + items_key_id: undefined, + errorDecrypting: false, + waitingForKey: false, + ...PayloadTimestampDefaults(), + updated_at_timestamp: 2, + }) + + const delta = new ItemsKeyDelta(ImmutablePayloadCollection.FromCollection(baseCollection), [payloadToIgnore]) + + const result = delta.result() + const updatedBasePayload = result.emits?.[0] as DecryptedPayload + + expect(updatedBasePayload.content.itemsKey).toBe('secret') + expect(updatedBasePayload.updated_at_timestamp).toBe(2) + expect(updatedBasePayload.dirty).toBeFalsy() + + const ignored = result.ignored?.[0] as EncryptedPayload + expect(ignored).toBeTruthy() + expect(isEncryptedPayload(ignored)).toBe(true) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.ts b/packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.ts new file mode 100644 index 000000000..4a8328aeb --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/ItemsKeyDelta.ts @@ -0,0 +1,52 @@ +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { + EncryptedPayloadInterface, + FullyFormedPayloadInterface, + isDecryptedPayload, + isEncryptedPayload, +} from '../../Abstract/Payload' +import { SourcelessSyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' +import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState' + +export class ItemsKeyDelta { + constructor( + private baseCollection: ImmutablePayloadCollection, + private readonly applyPayloads: FullyFormedPayloadInterface[], + ) {} + + public result(): SourcelessSyncDeltaEmit { + const emits: SyncResolvedPayload[] = [] + const ignored: EncryptedPayloadInterface[] = [] + + for (const apply of this.applyPayloads) { + const base = this.baseCollection.find(apply.uuid) + + if (!base) { + emits.push(payloadByFinalizingSyncState(apply, this.baseCollection)) + + continue + } + + if (isEncryptedPayload(apply) && isDecryptedPayload(base)) { + const keepBaseWithApplyTimestamps = base.copyAsSyncResolved({ + updated_at_timestamp: apply.updated_at_timestamp, + updated_at: apply.updated_at, + dirty: false, + lastSyncEnd: new Date(), + }) + + emits.push(keepBaseWithApplyTimestamps) + + ignored.push(apply) + } else { + emits.push(payloadByFinalizingSyncState(apply, this.baseCollection)) + } + } + + return { + emits: emits, + ignored, + } + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/OfflineSaved.ts b/packages/models/src/Domain/Runtime/Deltas/OfflineSaved.ts new file mode 100644 index 000000000..c2639107b --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/OfflineSaved.ts @@ -0,0 +1,33 @@ +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload' +import { OfflineSyncSavedContextualPayload } from '../../Abstract/Contextual/OfflineSyncSaved' +import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState' +import { SyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' +import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' + +export class DeltaOfflineSaved implements SyncDeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + readonly applyContextualPayloads: OfflineSyncSavedContextualPayload[], + ) {} + + public result(): SyncDeltaEmit { + const processed: SyncResolvedPayload[] = [] + + for (const apply of this.applyContextualPayloads) { + const base = this.baseCollection.find(apply.uuid) + + if (!base) { + continue + } + + processed.push(payloadByFinalizingSyncState(base, this.baseCollection)) + } + + return { + emits: processed, + source: PayloadEmitSource.OfflineSyncSaved, + } + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/OutOfSync.ts b/packages/models/src/Domain/Runtime/Deltas/OutOfSync.ts new file mode 100644 index 000000000..91540a549 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/OutOfSync.ts @@ -0,0 +1,62 @@ +import { PayloadEmitSource } from '../../Abstract/Payload' +import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' +import { PayloadContentsEqual } from '../../Utilities/Payload/PayloadContentsEqual' +import { ConflictDelta } from './Conflict' +import { ContentType } from '@standardnotes/common' +import { ItemsKeyDelta } from './ItemsKeyDelta' +import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { HistoryMap } from '../History' +import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' + +export class DeltaOutOfSync implements SyncDeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + readonly applyCollection: ImmutablePayloadCollection, + readonly historyMap: HistoryMap, + ) {} + + public result(): SyncDeltaEmit { + const result: SyncDeltaEmit = { + emits: [], + ignored: [], + source: PayloadEmitSource.RemoteRetrieved, + } + + for (const apply of this.applyCollection.all()) { + if (apply.content_type === ContentType.ItemsKey) { + const itemsKeyDeltaEmit = new ItemsKeyDelta(this.baseCollection, [apply]).result() + + extendSyncDelta(result, itemsKeyDeltaEmit) + + continue + } + + const base = this.baseCollection.find(apply.uuid) + + if (!base) { + result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection)) + + continue + } + + const isBaseDecrypted = isDecryptedPayload(base) + const isApplyDecrypted = isDecryptedPayload(apply) + + const needsConflict = + isApplyDecrypted !== isBaseDecrypted || + (isApplyDecrypted && isBaseDecrypted && !PayloadContentsEqual(apply, base)) + + if (needsConflict) { + const delta = new ConflictDelta(this.baseCollection, base, apply, this.historyMap) + + extendSyncDelta(result, delta.result()) + } else { + result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection)) + } + } + + return result + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts new file mode 100644 index 000000000..e78d27b6d --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts @@ -0,0 +1,41 @@ +import { ConflictDelta } from './Conflict' +import { PayloadEmitSource } from '../../Abstract/Payload' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { HistoryMap } from '../History' +import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' +import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState' + +export class DeltaRemoteDataConflicts implements SyncDeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + readonly applyCollection: ImmutablePayloadCollection, + readonly historyMap: HistoryMap, + ) {} + + public result(): SyncDeltaEmit { + const result: SyncDeltaEmit = { + emits: [], + ignored: [], + source: PayloadEmitSource.RemoteRetrieved, + } + + for (const apply of this.applyCollection.all()) { + const base = this.baseCollection.find(apply.uuid) + + const isBaseDeleted = base == undefined + + if (isBaseDeleted) { + result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection)) + + continue + } + + const delta = new ConflictDelta(this.baseCollection, base, apply, this.historyMap) + + extendSyncDelta(result, delta.result()) + } + + return result + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts new file mode 100644 index 000000000..d695d90de --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts @@ -0,0 +1,45 @@ +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload' +import { NoteContent } from '../../Syncable/Note' +import { PayloadCollection } from '../Collection/Payload/PayloadCollection' +import { DeltaRemoteRejected } from './RemoteRejected' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' + +describe('remote rejected delta', () => { + it('rejected payloads should not map onto app state', async () => { + const baseCollection = new PayloadCollection() + const basePayload = new DecryptedPayload({ + uuid: '123', + content_type: ContentType.Note, + dirty: true, + content: FillItemContent({ + title: 'foo', + }), + ...PayloadTimestampDefaults(), + updated_at_timestamp: 1, + }) + + baseCollection.set(basePayload) + + const rejectedPayload = basePayload.copy({ + content: FillItemContent({ + title: 'rejected', + }), + updated_at_timestamp: 3, + dirty: true, + }) + + const delta = new DeltaRemoteRejected( + ImmutablePayloadCollection.FromCollection(baseCollection), + ImmutablePayloadCollection.WithPayloads([rejectedPayload]), + ) + + const result = delta.result() + const payload = result.emits[0] as DecryptedPayload + + expect(payload.content.title).toBe('foo') + expect(payload.updated_at_timestamp).toBe(1) + expect(payload.dirty).toBeFalsy() + }) +}) diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts new file mode 100644 index 000000000..f52f8dc86 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts @@ -0,0 +1,40 @@ +import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource' +import { PayloadEmitSource } from '../../Abstract/Payload' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { SyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' +import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' + +export class DeltaRemoteRejected implements SyncDeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + readonly applyCollection: ImmutablePayloadCollection, + ) {} + + public result(): SyncDeltaEmit { + const results: SyncResolvedPayload[] = [] + + for (const apply of this.applyCollection.all()) { + const base = this.baseCollection.find(apply.uuid) + + if (!base) { + continue + } + + const result = base.copyAsSyncResolved( + { + dirty: false, + lastSyncEnd: new Date(), + }, + PayloadSource.RemoteSaved, + ) + + results.push(result) + } + + return { + emits: results, + source: PayloadEmitSource.RemoteSaved, + } + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.spec.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.spec.ts new file mode 100644 index 000000000..991fc2610 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.spec.ts @@ -0,0 +1,60 @@ +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { + DecryptedPayload, + EncryptedPayload, + isEncryptedPayload, + PayloadTimestampDefaults, +} from '../../Abstract/Payload' +import { PayloadCollection } from '../Collection/Payload/PayloadCollection' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { ItemsKeyContent } from '../../Syncable/ItemsKey/ItemsKeyInterface' +import { DeltaRemoteRetrieved } from './RemoteRetrieved' + +describe('remote retrieved delta', () => { + it('if local items key is decrypted, incoming encrypted should not overwrite', async () => { + const baseCollection = new PayloadCollection() + const basePayload = new DecryptedPayload({ + uuid: '123', + content_type: ContentType.ItemsKey, + content: FillItemContent({ + itemsKey: 'secret', + }), + ...PayloadTimestampDefaults(), + updated_at_timestamp: 1, + }) + + baseCollection.set(basePayload) + + const payloadToIgnore = new EncryptedPayload({ + uuid: '123', + content_type: ContentType.ItemsKey, + content: '004:...', + enc_item_key: '004:...', + items_key_id: undefined, + errorDecrypting: false, + waitingForKey: false, + ...PayloadTimestampDefaults(), + updated_at_timestamp: 2, + }) + + const delta = new DeltaRemoteRetrieved( + ImmutablePayloadCollection.FromCollection(baseCollection), + ImmutablePayloadCollection.WithPayloads([payloadToIgnore]), + [], + {}, + ) + + const result = delta.result() + + const updatedBasePayload = result.emits?.[0] as DecryptedPayload + + expect(updatedBasePayload.content.itemsKey).toBe('secret') + expect(updatedBasePayload.updated_at_timestamp).toBe(2) + expect(updatedBasePayload.dirty).toBeFalsy() + + const ignored = result.ignored?.[0] as EncryptedPayload + expect(ignored).toBeTruthy() + expect(isEncryptedPayload(ignored)).toBe(true) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts new file mode 100644 index 000000000..571098a9c --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts @@ -0,0 +1,87 @@ +import { ImmutablePayloadCollection } from './../Collection/Payload/ImmutablePayloadCollection' +import { ConflictDelta } from './Conflict' +import { isErrorDecryptingPayload, isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' +import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload' +import { ContentType, Uuid } from '@standardnotes/common' +import { HistoryMap } from '../History' +import { ServerSyncPushContextualPayload } from '../../Abstract/Contextual/ServerSyncPush' +import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState' +import { ItemsKeyDelta } from './ItemsKeyDelta' +import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' + +export class DeltaRemoteRetrieved implements SyncDeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + readonly applyCollection: ImmutablePayloadCollection, + private itemsSavedOrSaving: ServerSyncPushContextualPayload[], + readonly historyMap: HistoryMap, + ) {} + + private isUuidOfPayloadCurrentlySavingOrSaved(uuid: Uuid): boolean { + return this.itemsSavedOrSaving.find((i) => i.uuid === uuid) != undefined + } + + public result(): SyncDeltaEmit { + const result: SyncDeltaEmit = { + emits: [], + ignored: [], + source: PayloadEmitSource.RemoteRetrieved, + } + + const conflicted: FullyFormedPayloadInterface[] = [] + + /** + * If we have retrieved an item that was saved as part of this ongoing sync operation, + * or if the item is locally dirty, filter it out of retrieved_items, and add to potential conflicts. + */ + for (const apply of this.applyCollection.all()) { + if (apply.content_type === ContentType.ItemsKey) { + const itemsKeyDeltaEmit = new ItemsKeyDelta(this.baseCollection, [apply]).result() + + extendSyncDelta(result, itemsKeyDeltaEmit) + + continue + } + + const isSavedOrSaving = this.isUuidOfPayloadCurrentlySavingOrSaved(apply.uuid) + + if (isSavedOrSaving) { + conflicted.push(apply) + + continue + } + + const base = this.baseCollection.find(apply.uuid) + if (base?.dirty && !isErrorDecryptingPayload(base)) { + conflicted.push(apply) + + continue + } + + result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection)) + } + + /** + * For any potential conflict above, we compare the values with current + * local values, and if they differ, we create a new payload that is a copy + * of the server payload. + */ + for (const conflict of conflicted) { + if (!isDecryptedPayload(conflict)) { + continue + } + + const base = this.baseCollection.find(conflict.uuid) + if (!base) { + continue + } + + const delta = new ConflictDelta(this.baseCollection, base, conflict, this.historyMap) + + extendSyncDelta(result, delta.result()) + } + + return result + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts new file mode 100644 index 000000000..f3d74a945 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts @@ -0,0 +1,99 @@ +import { ServerSyncSavedContextualPayload } from './../../Abstract/Contextual/ServerSyncSaved' +import { DeletedPayload } from './../../Abstract/Payload/Implementations/DeletedPayload' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource' +import { isDeletedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' +import { PayloadEmitSource } from '../../Abstract/Payload' +import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState' +import { SyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' +import { BuildSyncResolvedParams, SyncResolvedPayload } from './Utilities/SyncResolvedPayload' +import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter' + +export class DeltaRemoteSaved implements SyncDeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + private readonly applyContextualPayloads: ServerSyncSavedContextualPayload[], + ) {} + + public result(): SyncDeltaEmit { + const processed: SyncResolvedPayload[] = [] + + for (const apply of this.applyContextualPayloads) { + const base = this.baseCollection.find(apply.uuid) + + if (!base) { + const discarded = new DeletedPayload( + { + ...apply, + deleted: true, + content: undefined, + ...BuildSyncResolvedParams({ + dirty: false, + lastSyncEnd: new Date(), + }), + }, + PayloadSource.RemoteSaved, + ) + + processed.push(discarded as SyncResolvedPayload) + continue + } + + /** + * If we save an item, but while in transit it is deleted locally, we want to keep + * local deletion status, and not old (false) deleted value that was sent to server. + */ + if (isDeletedPayload(base)) { + const baseWasDeletedAfterThisRequest = !apply.deleted + const regularDeletedPayload = apply.deleted + if (baseWasDeletedAfterThisRequest) { + const result = new DeletedPayload( + { + ...apply, + deleted: true, + content: undefined, + dirtyIndex: getIncrementedDirtyIndex(), + ...BuildSyncResolvedParams({ + dirty: true, + lastSyncEnd: new Date(), + }), + }, + PayloadSource.RemoteSaved, + ) + processed.push(result as SyncResolvedPayload) + } else if (regularDeletedPayload) { + const discarded = base.copy( + { + ...apply, + deleted: true, + ...BuildSyncResolvedParams({ + dirty: false, + lastSyncEnd: new Date(), + }), + }, + PayloadSource.RemoteSaved, + ) + processed.push(discarded as SyncResolvedPayload) + } + } else { + const result = payloadByFinalizingSyncState( + base.copy( + { + ...apply, + deleted: false, + }, + PayloadSource.RemoteSaved, + ), + this.baseCollection, + ) + processed.push(result) + } + } + + return { + emits: processed, + source: PayloadEmitSource.RemoteSaved, + } + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts new file mode 100644 index 000000000..a7d484b2e --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts @@ -0,0 +1,56 @@ +import { extendArray, filterFromArray, Uuids } from '@standardnotes/utils' +import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { PayloadsByAlternatingUuid } from '../../Utilities/Payload/PayloadsByAlternatingUuid' +import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' +import { PayloadEmitSource } from '../../Abstract/Payload' +import { SyncDeltaEmit } from './Abstract/DeltaEmit' +import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' +import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' + +/** + * UUID conflicts can occur if a user attmpts to import an old data + * backup with uuids from the old account into a new account. + * In uuid_conflict, we receive the value we attmpted to save. + */ +export class DeltaRemoteUuidConflicts implements SyncDeltaInterface { + constructor( + readonly baseCollection: ImmutablePayloadCollection, + readonly applyCollection: ImmutablePayloadCollection, + ) {} + + public result(): SyncDeltaEmit { + const results: SyncResolvedPayload[] = [] + const baseCollectionCopy = this.baseCollection.mutableCopy() + + for (const apply of this.applyCollection.all()) { + /** + * The payload in question may have been modified as part of alternating a uuid for + * another item. For example, alternating a uuid for a note will also affect the + * referencing tag, which would be added to `results`, but could also be inside + * of this.applyCollection. In this case we'd prefer the most recently modified value. + */ + const moreRecent = results.find((r) => r.uuid === apply.uuid) + const useApply = moreRecent || apply + + if (!isDecryptedPayload(useApply)) { + continue + } + + const alternateResults = PayloadsByAlternatingUuid( + useApply, + ImmutablePayloadCollection.FromCollection(baseCollectionCopy), + ) + + baseCollectionCopy.set(alternateResults) + + filterFromArray(results, (r) => Uuids(alternateResults).includes(r.uuid)) + + extendArray(results, alternateResults) + } + + return { + emits: results, + source: PayloadEmitSource.RemoteRetrieved, + } + } +} diff --git a/packages/models/src/Domain/Runtime/Deltas/Utilities/ApplyDirtyState.ts b/packages/models/src/Domain/Runtime/Deltas/Utilities/ApplyDirtyState.ts new file mode 100644 index 000000000..da85cb6dd --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/Utilities/ApplyDirtyState.ts @@ -0,0 +1,36 @@ +import { ImmutablePayloadCollection } from '../../Collection/Payload/ImmutablePayloadCollection' +import { FullyFormedPayloadInterface } from '../../../Abstract/Payload/Interfaces/UnionTypes' +import { SyncResolvedPayload } from './SyncResolvedPayload' +import { getIncrementedDirtyIndex } from '../../DirtyCounter/DirtyCounter' + +export function payloadByFinalizingSyncState( + payload: FullyFormedPayloadInterface, + baseCollection: ImmutablePayloadCollection, +): SyncResolvedPayload { + const basePayload = baseCollection.find(payload.uuid) + + if (!basePayload) { + return payload.copyAsSyncResolved({ + dirty: false, + lastSyncEnd: new Date(), + }) + } + + const stillDirty = + basePayload.dirtyIndex && basePayload.globalDirtyIndexAtLastSync + ? basePayload.dirtyIndex > basePayload.globalDirtyIndexAtLastSync + : false + + return payload.copyAsSyncResolved({ + dirty: stillDirty, + dirtyIndex: stillDirty ? getIncrementedDirtyIndex() : undefined, + lastSyncEnd: new Date(), + }) +} + +export function payloadsByFinalizingSyncState( + payloads: FullyFormedPayloadInterface[], + baseCollection: ImmutablePayloadCollection, +): SyncResolvedPayload[] { + return payloads.map((p) => payloadByFinalizingSyncState(p, baseCollection)) +} diff --git a/packages/models/src/Domain/Runtime/Deltas/Utilities/SyncResolvedPayload.ts b/packages/models/src/Domain/Runtime/Deltas/Utilities/SyncResolvedPayload.ts new file mode 100644 index 000000000..3cf048108 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/Utilities/SyncResolvedPayload.ts @@ -0,0 +1,12 @@ +import { FullyFormedPayloadInterface } from '../../../Abstract/Payload' + +export interface SyncResolvedParams { + dirty: boolean + lastSyncEnd: Date +} + +export function BuildSyncResolvedParams(params: SyncResolvedParams): SyncResolvedParams { + return params +} + +export type SyncResolvedPayload = SyncResolvedParams & FullyFormedPayloadInterface diff --git a/packages/models/src/Domain/Runtime/Deltas/index.ts b/packages/models/src/Domain/Runtime/Deltas/index.ts new file mode 100644 index 000000000..3ff1e240a --- /dev/null +++ b/packages/models/src/Domain/Runtime/Deltas/index.ts @@ -0,0 +1,10 @@ +export * from './Conflict' +export * from './FileImport' +export * from './OutOfSync' +export * from './RemoteDataConflicts' +export * from './RemoteRetrieved' +export * from './RemoteSaved' +export * from './OfflineSaved' +export * from './RemoteUuidConflicts' +export * from './RemoteRejected' +export * from './Abstract/DeltaEmit' diff --git a/packages/models/src/Domain/Runtime/DirtyCounter/DirtyCounter.ts b/packages/models/src/Domain/Runtime/DirtyCounter/DirtyCounter.ts new file mode 100644 index 000000000..b9deb7d2e --- /dev/null +++ b/packages/models/src/Domain/Runtime/DirtyCounter/DirtyCounter.ts @@ -0,0 +1,10 @@ +let dirtyIndex = 0 + +export function getIncrementedDirtyIndex() { + dirtyIndex++ + return dirtyIndex +} + +export function getCurrentDirtyIndex() { + return dirtyIndex +} diff --git a/packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts b/packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts new file mode 100644 index 000000000..0ed818617 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts @@ -0,0 +1,53 @@ +import { createNoteWithContent } from '../../Utilities/Test/SpecUtils' +import { ItemCollection } from '../Collection/Item/ItemCollection' +import { SNNote } from '../../Syncable/Note/Note' +import { itemsMatchingOptions } from './Search/SearchUtilities' +import { FilterDisplayOptions } from './DisplayOptions' + +describe('item display options', () => { + const collectionWithNotes = function (titles: (string | undefined)[] = [], bodies: string[] = []) { + const collection = new ItemCollection() + const notes: SNNote[] = [] + titles.forEach((title, index) => { + notes.push( + createNoteWithContent({ + title: title, + text: bodies[index], + }), + ) + }) + collection.set(notes) + return collection + } + + it('string query title', () => { + const query = 'foo' + + const options: FilterDisplayOptions = { + searchQuery: { query: query, includeProtectedNoteText: true }, + } + const collection = collectionWithNotes(['hello', 'fobar', 'foobar', 'foo']) + expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) + }) + + it('string query text', async function () { + const query = 'foo' + const options: FilterDisplayOptions = { + searchQuery: { query: query, includeProtectedNoteText: true }, + } + const collection = collectionWithNotes( + [undefined, undefined, undefined, undefined], + ['hello', 'fobar', 'foobar', 'foo'], + ) + expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) + }) + + it('string query title and text', async function () { + const query = 'foo' + const options: FilterDisplayOptions = { + searchQuery: { query: query, includeProtectedNoteText: true }, + } + const collection = collectionWithNotes(['hello', 'foobar'], ['foo', 'fobar']) + expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Display/DisplayOptions.ts b/packages/models/src/Domain/Runtime/Display/DisplayOptions.ts new file mode 100644 index 000000000..64a88771f --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/DisplayOptions.ts @@ -0,0 +1,25 @@ +import { ContentType } from '@standardnotes/common' +import { SmartView } from '../../Syncable/SmartView' +import { SNTag } from '../../Syncable/Tag' +import { CollectionSortDirection, CollectionSortProperty } from '../Collection/CollectionSort' +import { SearchQuery } from './Search/Types' +import { DisplayControllerCustomFilter } from './Types' + +export type DisplayOptions = FilterDisplayOptions & DisplayControllerOptions + +export interface FilterDisplayOptions { + tags?: SNTag[] + views?: SmartView[] + searchQuery?: SearchQuery + includePinned?: boolean + includeProtected?: boolean + includeTrashed?: boolean + includeArchived?: boolean +} + +export interface DisplayControllerOptions { + sortBy: CollectionSortProperty + sortDirection: CollectionSortDirection + hiddenContentTypes?: ContentType[] + customFilter?: DisplayControllerCustomFilter +} diff --git a/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts b/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts new file mode 100644 index 000000000..5143b71b1 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts @@ -0,0 +1,78 @@ +import { ContentType } from '@standardnotes/common' +import { DecryptedItem } from '../../Abstract/Item' +import { SNTag } from '../../Syncable/Tag' +import { CompoundPredicate } from '../Predicate/CompoundPredicate' +import { ItemWithTags } from './Search/ItemWithTags' +import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities' +import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types' +import { FilterDisplayOptions } from './DisplayOptions' + +export function computeUnifiedFilterForDisplayOptions( + options: FilterDisplayOptions, + collection: ReferenceLookupCollection, +): ItemFilter { + const filters = computeFiltersForDisplayOptions(options, collection) + + return (item: SearchableDecryptedItem) => { + return itemPassesFilters(item, filters) + } +} + +export function computeFiltersForDisplayOptions( + options: FilterDisplayOptions, + collection: ReferenceLookupCollection, +): ItemFilter[] { + const filters: ItemFilter[] = [] + + let viewsPredicate: CompoundPredicate | undefined = undefined + + if (options.views && options.views.length > 0) { + const compoundPredicate = new CompoundPredicate( + 'and', + options.views.map((t) => t.predicate), + ) + viewsPredicate = compoundPredicate + + filters.push((item) => { + if (compoundPredicate.keypathIncludesString('tags')) { + const noteWithTags = ItemWithTags.Create( + item.payload, + item, + collection.elementsReferencingElement(item, ContentType.Tag) as SNTag[], + ) + return compoundPredicate.matchesItem(noteWithTags) + } else { + return compoundPredicate.matchesItem(item) + } + }) + } + + if (options.tags && options.tags.length > 0) { + for (const tag of options.tags) { + filters.push((item) => tag.isReferencingItem(item)) + } + } + + if (options.includePinned === false && !viewsPredicate?.keypathIncludesString('pinned')) { + filters.push((item) => !item.pinned) + } + + if (options.includeProtected === false && !viewsPredicate?.keypathIncludesString('protected')) { + filters.push((item) => !item.protected) + } + + if (options.includeTrashed === false && !viewsPredicate?.keypathIncludesString('trashed')) { + filters.push((item) => !item.trashed) + } + + if (options.includeArchived === false && !viewsPredicate?.keypathIncludesString('archived')) { + filters.push((item) => !item.archived) + } + + if (options.searchQuery) { + const query = options.searchQuery + filters.push((item) => itemMatchesQuery(item, query, collection)) + } + + return filters +} diff --git a/packages/models/src/Domain/Runtime/Display/ItemDisplayController.spec.ts b/packages/models/src/Domain/Runtime/Display/ItemDisplayController.spec.ts new file mode 100644 index 000000000..f46fbd32b --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/ItemDisplayController.spec.ts @@ -0,0 +1,256 @@ +import { CreateItemDelta } from './../Index/ItemDelta' +import { DeletedPayload } from './../../Abstract/Payload/Implementations/DeletedPayload' +import { createFile, createNote, createTag, mockUuid, pinnedContent } from './../../Utilities/Test/SpecUtils' +import { ContentType } from '@standardnotes/common' +import { DeletedItem, EncryptedItem } from '../../Abstract/Item' +import { EncryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload' +import { createNoteWithContent } from '../../Utilities/Test/SpecUtils' +import { ItemCollection } from './../Collection/Item/ItemCollection' +import { ItemDisplayController } from './ItemDisplayController' +import { SNNote } from '../../Syncable/Note' + +describe('item display controller', () => { + it('should sort items', () => { + const collection = new ItemCollection() + const noteA = createNoteWithContent({ title: 'a' }) + const noteB = createNoteWithContent({ title: 'b' }) + collection.set([noteA, noteB]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + + expect(controller.items()[0]).toEqual(noteA) + expect(controller.items()[1]).toEqual(noteB) + + controller.setDisplayOptions({ sortBy: 'title', sortDirection: 'dsc' }) + + expect(controller.items()[0]).toEqual(noteB) + expect(controller.items()[1]).toEqual(noteA) + }) + + it('should filter items', () => { + const collection = new ItemCollection() + const noteA = createNoteWithContent({ title: 'a' }) + const noteB = createNoteWithContent({ title: 'b' }) + collection.set([noteA, noteB]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + + controller.setDisplayOptions({ + customFilter: (note) => { + return note.title !== 'a' + }, + }) + + expect(controller.items()).toHaveLength(1) + expect(controller.items()[0].title).toEqual('b') + }) + + it('should resort items after collection change', () => { + const collection = new ItemCollection() + const noteA = createNoteWithContent({ title: 'a' }) + collection.set([noteA]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + expect(controller.items()).toHaveLength(1) + + const noteB = createNoteWithContent({ title: 'b' }) + + const delta = CreateItemDelta({ changed: [noteB] }) + collection.onChange(delta) + controller.onCollectionChange(delta) + + expect(controller.items()).toHaveLength(2) + }) + + it('should not display encrypted items', () => { + const collection = new ItemCollection() + const noteA = new EncryptedItem( + new EncryptedPayload({ + uuid: mockUuid(), + content_type: ContentType.Note, + content: '004:...', + enc_item_key: '004:...', + items_key_id: mockUuid(), + errorDecrypting: true, + waitingForKey: false, + ...PayloadTimestampDefaults(), + }), + ) + collection.set([noteA]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + + expect(controller.items()).toHaveLength(0) + }) + + it('pinned items should come first', () => { + const collection = new ItemCollection() + const noteA = createNoteWithContent({ title: 'a' }) + const noteB = createNoteWithContent({ title: 'b' }) + collection.set([noteA, noteB]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + + expect(controller.items()[0]).toEqual(noteA) + expect(controller.items()[1]).toEqual(noteB) + + expect(collection.all()).toHaveLength(2) + + const pinnedNoteB = new SNNote( + noteB.payload.copy({ + content: { + ...noteB.content, + ...pinnedContent(), + }, + }), + ) + expect(pinnedNoteB.pinned).toBeTruthy() + + const delta = CreateItemDelta({ changed: [pinnedNoteB] }) + collection.onChange(delta) + controller.onCollectionChange(delta) + + expect(controller.items()[0]).toEqual(pinnedNoteB) + expect(controller.items()[1]).toEqual(noteA) + }) + + it('should not display deleted items', () => { + const collection = new ItemCollection() + const noteA = createNoteWithContent({ title: 'a' }) + collection.set([noteA]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + + const deletedItem = new DeletedItem( + new DeletedPayload({ + ...noteA.payload, + content: undefined, + deleted: true, + }), + ) + + const delta = CreateItemDelta({ changed: [deletedItem] }) + collection.onChange(delta) + controller.onCollectionChange(delta) + + expect(controller.items()).toHaveLength(0) + }) + + it('discarding elements should remove from display', () => { + const collection = new ItemCollection() + const noteA = createNoteWithContent({ title: 'a' }) + collection.set([noteA]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + + const delta = CreateItemDelta({ discarded: [noteA] as unknown as DeletedItem[] }) + collection.onChange(delta) + controller.onCollectionChange(delta) + + expect(controller.items()).toHaveLength(0) + }) + + it('should ignore items not matching content type on construction', () => { + const collection = new ItemCollection() + const note = createNoteWithContent({ title: 'a' }) + const tag = createTag() + collection.set([note, tag]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + expect(controller.items()).toHaveLength(1) + }) + + it('should ignore items not matching content type on sort change', () => { + const collection = new ItemCollection() + const note = createNoteWithContent({ title: 'a' }) + const tag = createTag() + collection.set([note, tag]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + controller.setDisplayOptions({ sortBy: 'created_at', sortDirection: 'asc' }) + expect(controller.items()).toHaveLength(1) + }) + + it('should ignore collection deltas with items not matching content types', () => { + const collection = new ItemCollection() + const note = createNoteWithContent({ title: 'a' }) + collection.set([note]) + + const controller = new ItemDisplayController(collection, [ContentType.Note], { + sortBy: 'title', + sortDirection: 'asc', + }) + const tag = createTag() + + const delta = CreateItemDelta({ inserted: [tag], changed: [note] }) + collection.onChange(delta) + controller.onCollectionChange(delta) + + expect(controller.items()).toHaveLength(1) + }) + + it('should display compound item types', () => { + const collection = new ItemCollection() + const note = createNoteWithContent({ title: 'Z' }) + const file = createFile('A') + collection.set([note, file]) + + const controller = new ItemDisplayController(collection, [ContentType.Note, ContentType.File], { + sortBy: 'title', + sortDirection: 'asc', + }) + + expect(controller.items()[0]).toEqual(file) + expect(controller.items()[1]).toEqual(note) + + controller.setDisplayOptions({ sortBy: 'title', sortDirection: 'dsc' }) + + expect(controller.items()[0]).toEqual(note) + expect(controller.items()[1]).toEqual(file) + }) + + it('should hide hidden types', () => { + const collection = new ItemCollection() + const note = createNote() + const file = createFile() + collection.set([note, file]) + + const controller = new ItemDisplayController(collection, [ContentType.Note, ContentType.File], { + sortBy: 'title', + sortDirection: 'asc', + }) + + expect(controller.items()).toHaveLength(2) + + controller.setDisplayOptions({ hiddenContentTypes: [ContentType.File] }) + + expect(controller.items()).toHaveLength(1) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts b/packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts new file mode 100644 index 000000000..60c35fe13 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts @@ -0,0 +1,138 @@ +import { ContentType } from '@standardnotes/common' +import { compareValues } from '@standardnotes/utils' +import { isDeletedItem, isEncryptedItem } from '../../Abstract/Item' +import { ItemDelta } from '../Index/ItemDelta' +import { DisplayControllerOptions } from './DisplayOptions' +import { sortTwoItems } from './SortTwoItems' +import { UuidToSortedPositionMap, DisplayItem, ReadonlyItemCollection } from './Types' + +export class ItemDisplayController { + private sortMap: UuidToSortedPositionMap = {} + private sortedItems: I[] = [] + private needsSort = true + + constructor( + private readonly collection: ReadonlyItemCollection, + public readonly contentTypes: ContentType[], + private options: DisplayControllerOptions, + ) { + this.filterThenSortElements(this.collection.all(this.contentTypes) as I[]) + } + + public items(): I[] { + return this.sortedItems + } + + setDisplayOptions(displayOptions: Partial): void { + this.options = { ...this.options, ...displayOptions } + this.needsSort = true + + this.filterThenSortElements(this.collection.all(this.contentTypes) as I[]) + } + + onCollectionChange(delta: ItemDelta): void { + const items = [...delta.changed, ...delta.inserted, ...delta.discarded].filter((i) => + this.contentTypes.includes(i.content_type), + ) + this.filterThenSortElements(items as I[]) + } + + private filterThenSortElements(elements: I[]): void { + for (const element of elements) { + const previousIndex = this.sortMap[element.uuid] + const previousElement = previousIndex != undefined ? this.sortedItems[previousIndex] : undefined + + const remove = () => { + if (previousIndex != undefined) { + delete this.sortMap[element.uuid] + + /** We don't yet remove the element directly from the array, since mutating + * the array inside a loop could render all other upcoming indexes invalid */ + ;(this.sortedItems[previousIndex] as unknown) = undefined + + /** Since an element is being removed from the array, we need to recompute + * the new positions for elements that are staying */ + this.needsSort = true + } + } + + if (isDeletedItem(element) || isEncryptedItem(element)) { + remove() + continue + } + + const passes = !this.collection.has(element.uuid) + ? false + : this.options.hiddenContentTypes?.includes(element.content_type) + ? false + : this.options.customFilter + ? this.options.customFilter(element) + : true + + if (passes) { + if (previousElement != undefined) { + /** Check to see if the element has changed its sort value. If so, we need to re-sort. */ + const previousValue = previousElement[this.options.sortBy] + + const newValue = element[this.options.sortBy] + + /** Replace the current element with the new one. */ + this.sortedItems[previousIndex] = element + + /** If the pinned status of the element has changed, it needs to be resorted */ + const pinChanged = previousElement.pinned !== element.pinned + + if (!compareValues(previousValue, newValue) || pinChanged) { + /** Needs resort because its re-sort value has changed, + * and thus its position might change */ + this.needsSort = true + } + } else { + /** Has not yet been inserted */ + this.sortedItems.push(element) + + /** Needs re-sort because we're just pushing the element to the end here */ + this.needsSort = true + } + } else { + /** Doesn't pass filter, remove from sorted and filtered */ + remove() + } + } + + if (this.needsSort) { + this.needsSort = false + this.resortItems() + } + } + + /** Resort the sortedItems array, and update the saved positions */ + private resortItems() { + const resorted = this.sortedItems.sort((a, b) => { + return sortTwoItems(a, b, this.options.sortBy, this.options.sortDirection) + }) + + /** + * Now that resorted contains the sorted elements (but also can contain undefined element) + * we create another array that filters out any of the undefinedes. We also keep track of the + * current index while we loop and set that in the this.sortMap. + * */ + const results = [] + let currentIndex = 0 + + /** @O(n) */ + for (const element of resorted) { + if (!element) { + continue + } + + results.push(element) + + this.sortMap[element.uuid] = currentIndex + + currentIndex++ + } + + this.sortedItems = results + } +} diff --git a/packages/models/src/Domain/Runtime/Display/Search/ItemWithTags.ts b/packages/models/src/Domain/Runtime/Display/Search/ItemWithTags.ts new file mode 100644 index 000000000..e92c2dc11 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Search/ItemWithTags.ts @@ -0,0 +1,36 @@ +import { SearchableDecryptedItem } from './Types' +import { ItemContent } from '../../../Abstract/Content/ItemContent' +import { DecryptedItem } from '../../../Abstract/Item' +import { DecryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/DecryptedPayload' +import { SNTag } from '../../../Syncable/Tag' + +interface ItemWithTagsContent extends ItemContent { + tags: SNTag[] +} + +export class ItemWithTags extends DecryptedItem implements SearchableDecryptedItem { + constructor( + payload: DecryptedPayloadInterface, + private item: SearchableDecryptedItem, + public readonly tags?: SNTag[], + ) { + super(payload) + this.tags = tags || payload.content.tags + } + + static Create(payload: DecryptedPayloadInterface, item: SearchableDecryptedItem, tags?: SNTag[]) { + return new ItemWithTags(payload as DecryptedPayloadInterface, item, tags) + } + + get tagsCount(): number { + return this.tags?.length || 0 + } + + get title(): string | undefined { + return this.item.title + } + + get text(): string | undefined { + return this.item.text + } +} diff --git a/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts new file mode 100644 index 000000000..b8b2db815 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts @@ -0,0 +1,97 @@ +import { ContentType } from '@standardnotes/common' +import { SNTag } from '../../../Syncable/Tag' +import { FilterDisplayOptions } from '../DisplayOptions' +import { computeFiltersForDisplayOptions } from '../DisplayOptionsToFilters' +import { SearchableItem } from './SearchableItem' +import { ReferenceLookupCollection, ItemFilter, SearchQuery, SearchableDecryptedItem } from './Types' + +enum MatchResult { + None = 0, + Title = 1, + Text = 2, + TitleAndText = Title + Text, + Uuid = 5, +} + +export function itemsMatchingOptions( + options: FilterDisplayOptions, + fromItems: SearchableDecryptedItem[], + collection: ReferenceLookupCollection, +): SearchableItem[] { + const filters = computeFiltersForDisplayOptions(options, collection) + + return fromItems.filter((item) => { + return itemPassesFilters(item, filters) + }) +} +export function itemPassesFilters(item: SearchableDecryptedItem, filters: ItemFilter[]) { + for (const filter of filters) { + if (!filter(item)) { + return false + } + } + return true +} + +export function itemMatchesQuery( + itemToMatch: SearchableDecryptedItem, + searchQuery: SearchQuery, + collection: ReferenceLookupCollection, +): boolean { + const itemTags = collection.elementsReferencingElement(itemToMatch, ContentType.Tag) as SNTag[] + const someTagsMatches = itemTags.some((tag) => matchResultForStringQuery(tag, searchQuery.query) !== MatchResult.None) + + if (itemToMatch.protected && !searchQuery.includeProtectedNoteText) { + const match = matchResultForStringQuery(itemToMatch, searchQuery.query) + return match === MatchResult.Title || match === MatchResult.TitleAndText || someTagsMatches + } + + return matchResultForStringQuery(itemToMatch, searchQuery.query) !== MatchResult.None || someTagsMatches +} + +function matchResultForStringQuery(item: SearchableItem, searchString: string): MatchResult { + if (searchString.length === 0) { + return MatchResult.TitleAndText + } + + const title = item.title?.toLowerCase() + const text = item.text?.toLowerCase() + const lowercaseText = searchString.toLowerCase() + const words = lowercaseText.split(' ') + const quotedText = stringBetweenQuotes(lowercaseText) + + if (quotedText) { + return ( + (title?.includes(quotedText) ? MatchResult.Title : MatchResult.None) + + (text?.includes(quotedText) ? MatchResult.Text : MatchResult.None) + ) + } + + if (stringIsUuid(lowercaseText)) { + return item.uuid === lowercaseText ? MatchResult.Uuid : MatchResult.None + } + + const matchesTitle = + title && + words.every((word) => { + return title.indexOf(word) >= 0 + }) + + const matchesBody = + text && + words.every((word) => { + return text.indexOf(word) >= 0 + }) + + return (matchesTitle ? MatchResult.Title : 0) + (matchesBody ? MatchResult.Text : 0) +} + +function stringBetweenQuotes(text: string) { + const matches = text.match(/"(.*?)"/) + return matches ? matches[1] : null +} + +function stringIsUuid(text: string) { + const matches = text.match(/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/) + return matches ? true : false +} diff --git a/packages/models/src/Domain/Runtime/Display/Search/SearchableItem.ts b/packages/models/src/Domain/Runtime/Display/Search/SearchableItem.ts new file mode 100644 index 000000000..48773f647 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Search/SearchableItem.ts @@ -0,0 +1,5 @@ +export interface SearchableItem { + uuid: string + title?: string + text?: string +} diff --git a/packages/models/src/Domain/Runtime/Display/Search/Types.ts b/packages/models/src/Domain/Runtime/Display/Search/Types.ts new file mode 100644 index 000000000..5e47f0111 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Search/Types.ts @@ -0,0 +1,16 @@ +import { ItemCollection } from './../../Collection/Item/ItemCollection' +import { DecryptedItemInterface } from '../../../Abstract/Item' +import { SearchableItem } from './SearchableItem' + +export type SearchQuery = { + query: string + includeProtectedNoteText: boolean +} + +export interface ReferenceLookupCollection { + elementsReferencingElement: ItemCollection['elementsReferencingElement'] +} + +export type SearchableDecryptedItem = SearchableItem & DecryptedItemInterface + +export type ItemFilter = (item: SearchableDecryptedItem) => boolean diff --git a/packages/models/src/Domain/Runtime/Display/SortTwoItems.spec.ts b/packages/models/src/Domain/Runtime/Display/SortTwoItems.spec.ts new file mode 100644 index 000000000..830c5518c --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/SortTwoItems.spec.ts @@ -0,0 +1,29 @@ +import { SortLeftFirst, SortRightFirst, sortTwoItems } from './SortTwoItems' +import { createNoteWithContent } from '../../Utilities/Test/SpecUtils' +import { SNNote } from '../../Syncable/Note' + +describe('sort two items', () => { + it('should sort correctly by dates', () => { + const noteA = createNoteWithContent({}, new Date(0)) + const noteB = createNoteWithContent({}, new Date(1)) + + expect(sortTwoItems(noteA, noteB, 'created_at', 'asc')).toEqual(SortLeftFirst) + expect(sortTwoItems(noteA, noteB, 'created_at', 'dsc')).toEqual(SortRightFirst) + }) + + it('should sort by title', () => { + const noteA = createNoteWithContent({ title: 'a' }) + const noteB = createNoteWithContent({ title: 'b' }) + + expect(sortTwoItems(noteA, noteB, 'title', 'asc')).toEqual(SortLeftFirst) + expect(sortTwoItems(noteA, noteB, 'title', 'dsc')).toEqual(SortRightFirst) + }) + + it('should sort correctly by title and pinned', () => { + const noteA = createNoteWithContent({ title: 'a' }) + const noteB = { ...createNoteWithContent({ title: 'b' }), pinned: true } as jest.Mocked + + expect(sortTwoItems(noteA, noteB, 'title', 'asc')).toEqual(SortRightFirst) + expect(sortTwoItems(noteA, noteB, 'title', 'dsc')).toEqual(SortRightFirst) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Display/SortTwoItems.ts b/packages/models/src/Domain/Runtime/Display/SortTwoItems.ts new file mode 100644 index 000000000..677170c83 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/SortTwoItems.ts @@ -0,0 +1,83 @@ +import { isString } from '@standardnotes/utils' +import { CollectionSort, CollectionSortDirection, CollectionSortProperty } from '../Collection/CollectionSort' +import { DisplayItem } from './Types' + +export const SortLeftFirst = -1 +export const SortRightFirst = 1 +export const KeepSameOrder = 0 + +/** @O(n * log(n)) */ +export function sortTwoItems( + a: DisplayItem | undefined, + b: DisplayItem | undefined, + sortBy: CollectionSortProperty, + sortDirection: CollectionSortDirection, + bypassPinCheck = false, +): number { + /** If the elements are undefined, move to beginning */ + if (!a) { + return SortLeftFirst + } + + if (!b) { + return SortRightFirst + } + + if (!bypassPinCheck) { + if (a.pinned && b.pinned) { + return sortTwoItems(a, b, sortBy, sortDirection, true) + } + if (a.pinned) { + return SortLeftFirst + } + if (b.pinned) { + return SortRightFirst + } + } + + const aValue = a[sortBy] || '' + const bValue = b[sortBy] || '' + const smallerNaturallyComesFirst = sortDirection === 'asc' + + let compareResult = KeepSameOrder + + /** + * Check for string length due to issue on React Native 0.65.1 + * where empty strings causes crash: + * https://github.com/facebook/react-native/issues/32174 + * */ + if ( + sortBy === CollectionSort.Title && + isString(aValue) && + isString(bValue) && + aValue.length > 0 && + bValue.length > 0 + ) { + compareResult = aValue.localeCompare(bValue, 'en', { numeric: true }) + } else if (aValue > bValue) { + compareResult = SortRightFirst + } else if (aValue < bValue) { + compareResult = SortLeftFirst + } else { + compareResult = KeepSameOrder + } + + const isLeftSmaller = compareResult === SortLeftFirst + const isLeftBigger = compareResult === SortRightFirst + + if (isLeftSmaller) { + if (smallerNaturallyComesFirst) { + return SortLeftFirst + } else { + return SortRightFirst + } + } else if (isLeftBigger) { + if (smallerNaturallyComesFirst) { + return SortRightFirst + } else { + return SortLeftFirst + } + } else { + return KeepSameOrder + } +} diff --git a/packages/models/src/Domain/Runtime/Display/Types.ts b/packages/models/src/Domain/Runtime/Display/Types.ts new file mode 100644 index 000000000..feb607fc7 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Types.ts @@ -0,0 +1,13 @@ +import { Uuid } from '@standardnotes/common' +import { DecryptedItemInterface } from '../../Abstract/Item' +import { SortableItem } from '../Collection/CollectionSort' +import { ItemCollection } from '../Collection/Item/ItemCollection' + +export type DisplayControllerCustomFilter = (element: DisplayItem) => boolean +export type UuidToSortedPositionMap = Record +export type DisplayItem = SortableItem & DecryptedItemInterface + +export interface ReadonlyItemCollection { + all: ItemCollection['all'] + has: ItemCollection['has'] +} diff --git a/packages/models/src/Domain/Runtime/Display/index.ts b/packages/models/src/Domain/Runtime/Display/index.ts new file mode 100644 index 000000000..6e66f2c27 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/index.ts @@ -0,0 +1,8 @@ +export * from './DisplayOptions' +export * from './DisplayOptionsToFilters' +export * from './ItemDisplayController' +export * from './Search/ItemWithTags' +export * from './Search/SearchableItem' +export * from './Search/SearchUtilities' +export * from './Search/Types' +export * from './Types' diff --git a/packages/models/src/Domain/Runtime/History/Generator.ts b/packages/models/src/Domain/Runtime/History/Generator.ts new file mode 100644 index 000000000..9f14ea3ee --- /dev/null +++ b/packages/models/src/Domain/Runtime/History/Generator.ts @@ -0,0 +1,24 @@ +import { DecryptedPayloadInterface } from './../../Abstract/Payload/Interfaces/DecryptedPayload' +import { ContentType } from '@standardnotes/common' +import { NoteContent } from '../../Syncable/Note' +import { HistoryEntry } from './HistoryEntry' +import { NoteHistoryEntry } from './NoteHistoryEntry' + +export function CreateHistoryEntryForPayload( + payload: DecryptedPayloadInterface, + previousEntry?: HistoryEntry, +): HistoryEntry { + const type = payload.content_type + const historyItemClass = historyClassForContentType(type) + const entry = new historyItemClass(payload, previousEntry) + return entry +} + +function historyClassForContentType(contentType: ContentType) { + switch (contentType) { + case ContentType.Note: + return NoteHistoryEntry + default: + return HistoryEntry + } +} diff --git a/packages/models/src/Domain/Runtime/History/HistoryEntry.ts b/packages/models/src/Domain/Runtime/History/HistoryEntry.ts new file mode 100644 index 000000000..1444e49b5 --- /dev/null +++ b/packages/models/src/Domain/Runtime/History/HistoryEntry.ts @@ -0,0 +1,98 @@ +import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem' +import { DecryptedPayloadInterface } from './../../Abstract/Payload/Interfaces/DecryptedPayload' +import { isNullOrUndefined } from '@standardnotes/utils' +import { CreateDecryptedItemFromPayload } from '../../Utilities/Item/ItemGenerator' +import { NoteContent } from '../../Syncable/Note' +import { HistoryEntryInterface } from './HistoryEntryInterface' + +export class HistoryEntry implements HistoryEntryInterface { + public readonly payload: DecryptedPayloadInterface + public readonly previousEntry?: HistoryEntry + protected readonly defaultContentKeyToDiffOn: keyof NoteContent = 'text' + protected readonly textCharDiffLength: number + protected readonly hasPreviousEntry: boolean + + constructor(payload: DecryptedPayloadInterface, previousEntry?: HistoryEntry) { + this.payload = payload.copy() + this.previousEntry = previousEntry + this.hasPreviousEntry = !isNullOrUndefined(previousEntry) + /** We'll try to compute the delta based on an assumed + * content property of `text`, if it exists. */ + const propertyValue = this.payload.content[this.defaultContentKeyToDiffOn] as string | undefined + + if (propertyValue) { + if (previousEntry) { + const previousValue = (previousEntry.payload.content[this.defaultContentKeyToDiffOn] as string)?.length || 0 + this.textCharDiffLength = propertyValue.length - previousValue + } else { + this.textCharDiffLength = propertyValue.length + } + } else { + this.textCharDiffLength = 0 + } + } + + public itemFromPayload(): DecryptedItemInterface { + return CreateDecryptedItemFromPayload(this.payload) + } + + public isSameAsEntry(entry: HistoryEntry): boolean { + if (!entry) { + return false + } + const lhs = this.itemFromPayload() + const rhs = entry.itemFromPayload() + const datesEqual = lhs.userModifiedDate.getTime() === rhs.userModifiedDate.getTime() + if (!datesEqual) { + return false + } + /** Dates are the same, but because JS is only accurate to milliseconds, + * items can have different content but same dates */ + return lhs.isItemContentEqualWith(rhs) + } + + public isDiscardable(): boolean { + return false + } + + public operationVector(): number { + /** + * We'll try to use the value of `textCharDiffLength` + * to help determine this, if it's set + */ + if (this.textCharDiffLength !== undefined) { + if (!this.hasPreviousEntry || this.textCharDiffLength === 0) { + return 0 + } else if (this.textCharDiffLength < 0) { + return -1 + } else { + return 1 + } + } + + /** Otherwise use a default value of 1 */ + return 1 + } + + public deltaSize(): number { + /** + * Up to the subclass to determine how large the delta was, + * i.e number of characters changed. + * But this general class won't be able to determine which property it + * should diff on, or even its format. + */ + /** + * We can return the `textCharDiffLength` if it's set, + * otherwise, just return 1; + */ + if (this.textCharDiffLength !== undefined) { + return Math.abs(this.textCharDiffLength) + } + /** + * Otherwise return 1 here to constitute a basic positive delta. + * The value returned should always be positive. Override `operationVector` + * to return the direction of the delta. + */ + return 1 + } +} diff --git a/packages/models/src/Domain/Runtime/History/HistoryEntryInterface.ts b/packages/models/src/Domain/Runtime/History/HistoryEntryInterface.ts new file mode 100644 index 000000000..73066ec7d --- /dev/null +++ b/packages/models/src/Domain/Runtime/History/HistoryEntryInterface.ts @@ -0,0 +1,13 @@ +import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { NoteContent } from '../../Syncable/Note/NoteContent' + +export interface HistoryEntryInterface { + readonly payload: DecryptedPayloadInterface + readonly previousEntry?: HistoryEntryInterface + itemFromPayload(): DecryptedItemInterface + isSameAsEntry(entry: HistoryEntryInterface): boolean + isDiscardable(): boolean + operationVector(): number + deltaSize(): number +} diff --git a/packages/models/src/Domain/Runtime/History/HistoryMap.ts b/packages/models/src/Domain/Runtime/History/HistoryMap.ts new file mode 100644 index 000000000..826415f80 --- /dev/null +++ b/packages/models/src/Domain/Runtime/History/HistoryMap.ts @@ -0,0 +1,10 @@ +import { Uuid } from '@standardnotes/common' +import { HistoryEntry } from './HistoryEntry' + +export type HistoryMap = Record + +export const historyMapFunctions = { + getNewestRevision: (history: HistoryEntry[]): HistoryEntry | undefined => { + return history[0] + }, +} diff --git a/packages/models/src/Domain/Runtime/History/NoteHistoryEntry.ts b/packages/models/src/Domain/Runtime/History/NoteHistoryEntry.ts new file mode 100644 index 000000000..bffdbb60e --- /dev/null +++ b/packages/models/src/Domain/Runtime/History/NoteHistoryEntry.ts @@ -0,0 +1,28 @@ +import { isEmpty } from '@standardnotes/utils' +import { HistoryEntry } from './HistoryEntry' + +export class NoteHistoryEntry extends HistoryEntry { + previewTitle(): string { + if (this.payload.updated_at.getTime() > 0) { + return this.payload.updated_at.toLocaleString() + } else { + return this.payload.created_at.toLocaleString() + } + } + + previewSubTitle(): string { + if (!this.hasPreviousEntry) { + return `${this.textCharDiffLength} characters loaded` + } else if (this.textCharDiffLength < 0) { + return `${this.textCharDiffLength * -1} characters removed` + } else if (this.textCharDiffLength > 0) { + return `${this.textCharDiffLength} characters added` + } else { + return 'Title or metadata changed' + } + } + + public override isDiscardable(): boolean { + return isEmpty(this.payload.content.text) + } +} diff --git a/packages/models/src/Domain/Runtime/History/index.ts b/packages/models/src/Domain/Runtime/History/index.ts new file mode 100644 index 000000000..aee5b736d --- /dev/null +++ b/packages/models/src/Domain/Runtime/History/index.ts @@ -0,0 +1,5 @@ +export * from './Generator' +export * from './HistoryEntry' +export * from './HistoryMap' +export * from './NoteHistoryEntry' +export * from './HistoryEntryInterface' diff --git a/packages/models/src/Domain/Runtime/Index/ItemDelta.ts b/packages/models/src/Domain/Runtime/Index/ItemDelta.ts new file mode 100644 index 000000000..7a205157e --- /dev/null +++ b/packages/models/src/Domain/Runtime/Index/ItemDelta.ts @@ -0,0 +1,24 @@ +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedItemInterface, DeletedItemInterface, EncryptedItemInterface } from '../../Abstract/Item' +import { AnyItemInterface } from '../../Abstract/Item/Interfaces/UnionTypes' + +export interface ItemDelta { + changed: AnyItemInterface[] + inserted: AnyItemInterface[] + /** Items that were deleted and finished sync */ + discarded: DeletedItemInterface[] + /** Items which have encrypted overwrite protection enabled */ + ignored: EncryptedItemInterface[] + /** Items which were previously error decrypting which have now been successfully decrypted */ + unerrored: DecryptedItemInterface[] +} + +export function CreateItemDelta(partial: Partial): ItemDelta { + return { + changed: partial.changed || [], + inserted: partial.inserted || [], + discarded: partial.discarded || [], + ignored: partial.ignored || [], + unerrored: partial.unerrored || [], + } +} diff --git a/packages/models/src/Domain/Runtime/Index/SNIndex.ts b/packages/models/src/Domain/Runtime/Index/SNIndex.ts new file mode 100644 index 000000000..94f2fc49c --- /dev/null +++ b/packages/models/src/Domain/Runtime/Index/SNIndex.ts @@ -0,0 +1,5 @@ +import { ItemDelta } from './ItemDelta' + +export interface SNIndex { + onChange(delta: ItemDelta): void +} diff --git a/packages/models/src/Domain/Runtime/Predicate/CompoundPredicate.ts b/packages/models/src/Domain/Runtime/Predicate/CompoundPredicate.ts new file mode 100644 index 000000000..1b27e2995 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/CompoundPredicate.ts @@ -0,0 +1,46 @@ +import { PredicateTarget, PredicateCompoundOperator, PredicateInterface, PredicateJsonForm } from './Interface' + +export class CompoundPredicate implements PredicateInterface { + constructor( + public readonly operator: PredicateCompoundOperator, + public readonly predicates: PredicateInterface[], + ) {} + + matchesItem(item: T): boolean { + if (this.operator === 'and') { + for (const subPredicate of this.predicates) { + if (!subPredicate.matchesItem(item)) { + return false + } + } + return true + } + + if (this.operator === 'or') { + for (const subPredicate of this.predicates) { + if (subPredicate.matchesItem(item)) { + return true + } + } + return false + } + + return false + } + + keypathIncludesString(verb: string): boolean { + for (const subPredicate of this.predicates) { + if (subPredicate.keypathIncludesString(verb)) { + return true + } + } + return false + } + + toJson(): PredicateJsonForm { + return { + operator: this.operator, + value: this.predicates.map((predicate) => predicate.toJson()), + } + } +} diff --git a/packages/models/src/Domain/Runtime/Predicate/Generators.ts b/packages/models/src/Domain/Runtime/Predicate/Generators.ts new file mode 100644 index 000000000..d8bb6d0f3 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/Generators.ts @@ -0,0 +1,140 @@ +import { CompoundPredicate } from './CompoundPredicate' +import { IncludesPredicate } from './IncludesPredicate' +import { + AllPredicateCompoundOperators, + PredicateCompoundOperator, + PredicateInterface, + PredicateOperator, + SureValue, + PredicateJsonForm, + AllPredicateOperators, + RawPredicateInArrayForm, + SureValueNonObjectTypesAsStrings, + StringKey, + PredicateTarget, +} from './Interface' +import { NotPredicate } from './NotPredicate' +import { Predicate } from './Predicate' + +export function predicateFromArguments( + keypath: StringKey | undefined, + operator: PredicateOperator, + value: SureValue | PredicateJsonForm, +): PredicateInterface { + if (AllPredicateCompoundOperators.includes(operator as PredicateCompoundOperator)) { + return compoundPredicateFromArguments(operator, value as unknown as PredicateJsonForm[]) + } else if (operator === 'not') { + return new NotPredicate(predicateFromJson(value as PredicateJsonForm)) + } else if (operator === 'includes' && keypath) { + if (isSureValue(value)) { + return new Predicate(keypath, operator, value) + } else { + return new IncludesPredicate(keypath, predicateFromJson(value as PredicateJsonForm)) + } + } else if (keypath) { + return new Predicate(keypath, operator, value as SureValue) + } + + throw Error('Invalid predicate arguments') +} + +export function compoundPredicateFromArguments( + operator: PredicateOperator, + value: PredicateJsonForm[], +): PredicateInterface { + const subPredicates = value.map((jsonPredicate) => { + return predicateFromJson(jsonPredicate) + }) + return new CompoundPredicate(operator as PredicateCompoundOperator, subPredicates) +} + +export function notPredicateFromArguments(value: PredicateJsonForm): PredicateInterface { + const subPredicate = predicateFromJson(value) + return new NotPredicate(subPredicate) +} + +export function includesPredicateFromArguments( + keypath: StringKey, + value: PredicateJsonForm, +): PredicateInterface { + const subPredicate = predicateFromJson(value) + return new IncludesPredicate(keypath, subPredicate) +} + +export function predicateFromJson(values: PredicateJsonForm): PredicateInterface { + if (Array.isArray(values)) { + throw Error('Invalid predicateFromJson value') + } + return predicateFromArguments( + values.keypath as StringKey, + values.operator, + isValuePredicateInArrayForm(values.value) + ? predicateDSLArrayToJsonPredicate(values.value) + : (values.value as PredicateJsonForm), + ) +} + +export function predicateFromDSLString(dsl: string): PredicateInterface { + try { + const components = JSON.parse(dsl.substring(1, dsl.length)) as string[] + components.shift() + const predicateJson = predicateDSLArrayToJsonPredicate(components as RawPredicateInArrayForm) + return predicateFromJson(predicateJson) + } catch (e) { + throw Error(`Invalid smart view syntax ${e}`) + } +} + +function isValuePredicateInArrayForm( + value: SureValue | PredicateJsonForm | PredicateJsonForm[] | RawPredicateInArrayForm, +): value is RawPredicateInArrayForm { + return Array.isArray(value) && AllPredicateOperators.includes(value[1] as PredicateOperator) +} + +function isSureValue(value: unknown): value is SureValue { + if (SureValueNonObjectTypesAsStrings.includes(typeof value)) { + return true + } + + if (Array.isArray(value)) { + return !isValuePredicateInArrayForm(value) + } + + return false +} + +function predicateDSLArrayToJsonPredicate(predicateArray: RawPredicateInArrayForm): PredicateJsonForm { + const predicateValue = predicateArray[2] as + | SureValue + | SureValue[] + | RawPredicateInArrayForm + | RawPredicateInArrayForm[] + + let resolvedPredicateValue: PredicateJsonForm | SureValue | PredicateJsonForm[] + + if (Array.isArray(predicateValue)) { + const level1CondensedValue = predicateValue as SureValue[] | RawPredicateInArrayForm | RawPredicateInArrayForm[] + + if (Array.isArray(level1CondensedValue[0])) { + const level2CondensedValue = level1CondensedValue as RawPredicateInArrayForm[] + resolvedPredicateValue = level2CondensedValue.map((subPredicate) => + predicateDSLArrayToJsonPredicate(subPredicate), + ) + } else if (isValuePredicateInArrayForm(predicateValue[1])) { + const level2CondensedValue = level1CondensedValue as RawPredicateInArrayForm + resolvedPredicateValue = predicateDSLArrayToJsonPredicate(level2CondensedValue) + } else { + const level2CondensedValue = predicateValue as SureValue + resolvedPredicateValue = level2CondensedValue + } + } else { + const level1CondensedValue = predicateValue as SureValue + resolvedPredicateValue = level1CondensedValue + } + + return { + keypath: predicateArray[0], + operator: predicateArray[1] as PredicateOperator, + value: resolvedPredicateValue, + } +} diff --git a/packages/models/src/Domain/Runtime/Predicate/IncludesPredicate.ts b/packages/models/src/Domain/Runtime/Predicate/IncludesPredicate.ts new file mode 100644 index 000000000..afc3fc630 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/IncludesPredicate.ts @@ -0,0 +1,32 @@ +import { PredicateTarget, PredicateInterface, PredicateJsonForm, StringKey } from './Interface' + +export class IncludesPredicate implements PredicateInterface { + constructor(private readonly keypath: StringKey, public readonly predicate: PredicateInterface) {} + + matchesItem(item: T): boolean { + const keyPathComponents = this.keypath.split('.') as StringKey[] + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const valueAtKeyPath: T = keyPathComponents.reduce((previous, current) => { + return previous && previous[current] + }, item) + + if (!Array.isArray(valueAtKeyPath)) { + return false + } + + return valueAtKeyPath.some((subItem) => this.predicate.matchesItem(subItem)) + } + + keypathIncludesString(verb: string): boolean { + return this.keypath.includes(verb) + } + + toJson(): PredicateJsonForm { + return { + keypath: this.keypath, + operator: 'includes', + value: this.predicate.toJson(), + } + } +} diff --git a/packages/models/src/Domain/Runtime/Predicate/Interface.ts b/packages/models/src/Domain/Runtime/Predicate/Interface.ts new file mode 100644 index 000000000..85c3c616f --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/Interface.ts @@ -0,0 +1,45 @@ +export interface PredicateInterface { + matchesItem(item: T): boolean + keypathIncludesString(verb: string): boolean + toJson(): PredicateJsonForm +} + +export type RawPredicateInArrayForm = string[] + +export interface PredicateJsonForm { + keypath?: string + operator: PredicateOperator + value: SureValue | PredicateJsonForm | PredicateJsonForm[] | RawPredicateInArrayForm +} + +export const AllPredicateCompoundOperators = ['and', 'or'] as const +export type PredicateCompoundOperator = typeof AllPredicateCompoundOperators[number] + +export const AllPredicateOperators = [ + ...AllPredicateCompoundOperators, + '!=', + '=', + '<', + '>', + '<=', + '>=', + 'startsWith', + 'in', + 'matches', + 'not', + 'includes', +] as const + +export type PredicateOperator = typeof AllPredicateOperators[number] + +export type SureValue = number | number[] | string[] | string | Date | boolean | false | '' + +export const SureValueNonObjectTypesAsStrings = ['number', 'string', 'boolean'] + +export type FalseyValue = false | '' | null | undefined + +export type PrimitiveOperand = SureValue | FalseyValue + +export type PredicateTarget = unknown + +export type StringKey = keyof T & string diff --git a/packages/models/src/Domain/Runtime/Predicate/NotPredicate.ts b/packages/models/src/Domain/Runtime/Predicate/NotPredicate.ts new file mode 100644 index 000000000..536192150 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/NotPredicate.ts @@ -0,0 +1,20 @@ +import { PredicateTarget, PredicateInterface, PredicateJsonForm } from './Interface' + +export class NotPredicate implements PredicateInterface { + constructor(public readonly predicate: PredicateInterface) {} + + matchesItem(item: T): boolean { + return !this.predicate.matchesItem(item) + } + + keypathIncludesString(verb: string): boolean { + return this.predicate.keypathIncludesString(verb) + } + + toJson(): PredicateJsonForm { + return { + operator: 'not', + value: this.predicate.toJson(), + } + } +} diff --git a/packages/models/src/Domain/Runtime/Predicate/Operator.ts b/packages/models/src/Domain/Runtime/Predicate/Operator.ts new file mode 100644 index 000000000..a1af4db6a --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/Operator.ts @@ -0,0 +1,95 @@ +import { isString } from '@standardnotes/utils' +import { FalseyValue, PredicateOperator, PrimitiveOperand, SureValue } from './Interface' +import { dateFromDSLDateString } from './Utils' + +export function valueMatchesTargetValue( + value: PrimitiveOperand, + operator: PredicateOperator, + targetValue: SureValue, +): boolean { + if (targetValue == undefined) { + return false + } + + if (typeof targetValue === 'string' && targetValue.includes('.ago')) { + targetValue = dateFromDSLDateString(targetValue) + } + + if (typeof targetValue === 'string') { + targetValue = targetValue.toLowerCase() + } + + if (typeof value === 'string') { + value = value.toLowerCase() + } + + if (operator === 'not') { + return !valueMatchesTargetValue(value, '=', targetValue) + } + + const falseyValues = [false, '', null, undefined, NaN] + if (value == undefined) { + const isExpectingFalseyValue = falseyValues.includes(targetValue as FalseyValue) + if (operator === '!=') { + return !isExpectingFalseyValue + } else { + return isExpectingFalseyValue + } + } + + if (operator === '=') { + if (Array.isArray(value)) { + return JSON.stringify(value) === JSON.stringify(targetValue) + } else { + return value === targetValue + } + } + + if (operator === '!=') { + if (Array.isArray(value)) { + return JSON.stringify(value) !== JSON.stringify(targetValue) + } else { + return value !== targetValue + } + } + + if (operator === '<') { + return (value as number) < (targetValue as number) + } + + if (operator === '>') { + return (value as number) > (targetValue as number) + } + + if (operator === '<=') { + return (value as number) <= (targetValue as number) + } + + if (operator === '>=') { + return (value as number) >= (targetValue as number) + } + + if (operator === 'startsWith') { + return (value as string).startsWith(targetValue as string) + } + + if (operator === 'in' && Array.isArray(targetValue)) { + return (targetValue as SureValue[]).includes(value) + } + + if (operator === 'includes') { + if (isString(value)) { + return value.includes(targetValue as string) + } + + if (isString(targetValue) && (isString(value) || Array.isArray(value))) { + return (value as SureValue[]).includes(targetValue) + } + } + + if (operator === 'matches') { + const regex = new RegExp(targetValue as string) + return regex.test(value as string) + } + return false +} diff --git a/packages/models/src/Domain/Runtime/Predicate/Predicate.spec.ts b/packages/models/src/Domain/Runtime/Predicate/Predicate.spec.ts new file mode 100644 index 000000000..974d2d1f4 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/Predicate.spec.ts @@ -0,0 +1,639 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' +import { ContentType } from '@standardnotes/common' +import { + compoundPredicateFromArguments, + includesPredicateFromArguments, + notPredicateFromArguments, + predicateFromArguments, + predicateFromDSLString, +} from './Generators' +import { IncludesPredicate } from './IncludesPredicate' +import { Predicate } from './Predicate' +import { CompoundPredicate } from './CompoundPredicate' +import { NotPredicate } from './NotPredicate' + +interface Item extends ItemInterface { + content_type: ContentType + updated_at: Date +} + +interface Note extends Item { + title: string + text: string + tags: Tag[] +} + +interface Tag extends Item { + title: string +} + +function createNote(content: Record, tags?: Tag[]): Note { + return { + ...content, + content_type: ContentType.Note, + tags, + } as jest.Mocked +} + +function createTag(title: string): Tag { + return { + title, + content_type: ContentType.Tag, + } as jest.Mocked +} + +function createItem(content: Record, updatedAt?: Date): Item { + return { + ...content, + updated_at: updatedAt, + content_type: ContentType.Any, + } as jest.Mocked +} + +const createNoteContent = (title = 'Hello', desc = 'World') => { + const params = { + title: title, + text: desc, + } + return params +} + +const tags = [createTag('foo'), createTag('bar'), createTag('far')] + +describe('predicates', () => { + it('string comparisons should be case insensitive', () => { + const string = '!["Not notes", "title", "startsWith", "foo"]' + const predicate = predicateFromDSLString(string) + + const matchingItem1 = createTag('foo') + + expect(predicate.matchesItem(matchingItem1)).toEqual(true) + + const matchingItem2 = { + title: 'Foo', + } as jest.Mocked + + expect(predicate.matchesItem(matchingItem2)).toEqual(true) + }) + + describe('includes operator', () => { + let item: Note + beforeEach(() => { + item = createNote(createNoteContent(), tags) + }) + + it('includes string', () => { + expect(new Predicate('title', 'includes', 'ello').matchesItem(item)).toEqual(true) + }) + }) + + describe('or operator', () => { + let item: Note + const title = 'Hello' + beforeEach(() => { + item = createNote(createNoteContent(title)) + }) + + it('both matching', () => { + expect( + compoundPredicateFromArguments('or', [ + { keypath: 'title', operator: '=', value: 'Hello' }, + { keypath: 'content_type', operator: '=', value: ContentType.Note }, + ]).matchesItem(item), + ).toEqual(true) + }) + + it('first matching', () => { + expect( + compoundPredicateFromArguments('or', [ + { keypath: 'title', operator: '=', value: 'Hello' }, + { keypath: 'content_type', operator: '=', value: 'Wrong' }, + ]).matchesItem(item), + ).toEqual(true) + }) + + it('second matching', () => { + expect( + compoundPredicateFromArguments('or', [ + { keypath: 'title', operator: '=', value: 'Wrong' }, + { keypath: 'content_type', operator: '=', value: ContentType.Note }, + ]).matchesItem(item), + ).toEqual(true) + }) + + it('both nonmatching', () => { + expect( + compoundPredicateFromArguments('or', [ + { keypath: 'title', operator: '=', value: 'Wrong' }, + { keypath: 'content_type', operator: '=', value: 'Wrong' }, + ]).matchesItem(item), + ).toEqual(false) + }) + }) + + describe('includes operator', () => { + let item: Note + const title = 'Foo' + beforeEach(() => { + item = createNote(createNoteContent(title), tags) + }) + + it('all matching', () => { + const predicate = new IncludesPredicate('tags', new Predicate('title', 'in', ['sobar', 'foo'])) + + expect(predicate.matchesItem(item)).toEqual(true) + }) + }) + + describe('and operator', () => { + let item: Note + const title = 'Foo' + beforeEach(() => { + item = createNote(createNoteContent(title)) + }) + + it('all matching', () => { + expect( + compoundPredicateFromArguments('and', [ + { keypath: 'title', operator: '=', value: title }, + { keypath: 'content_type', operator: '=', value: ContentType.Note }, + ]).matchesItem(item), + ).toEqual(true) + }) + + it('one matching', () => { + expect( + compoundPredicateFromArguments('and', [ + { keypath: 'title', operator: '=', value: 'Wrong' }, + { keypath: 'content_type', operator: '=', value: ContentType.Note }, + ]).matchesItem(item), + ).toEqual(false) + }) + + it('none matching', () => { + expect( + compoundPredicateFromArguments('and', [ + { keypath: 'title', operator: '=', value: '123' }, + { keypath: 'content_type', operator: '=', value: '456' }, + ]).matchesItem(item), + ).toEqual(false) + }) + + it('explicit compound syntax', () => { + const compoundProd = new CompoundPredicate('and', [ + new Predicate('title', '=', title), + new Predicate('content_type', '=', ContentType.Note), + ]) + expect(compoundProd.matchesItem(item)).toEqual(true) + }) + }) + + describe('not operator', function () { + let item: Note + beforeEach(() => { + item = createNote(createNoteContent(), tags) + }) + + it('basic not predicate', () => { + expect( + new NotPredicate( + new IncludesPredicate('tags', new Predicate('title', '=', 'far')), + ).matchesItem(item), + ).toEqual(false) + }) + + it('recursive compound predicate', () => { + expect( + new CompoundPredicate('and', [ + new NotPredicate(new IncludesPredicate('tags', new Predicate('title', '=', 'far'))), + new IncludesPredicate('tags', new Predicate('title', '=', 'foo')), + ]).matchesItem(item), + ).toEqual(false) + }) + + it('matching basic operator', () => { + expect( + notPredicateFromArguments({ + keypath: 'title', + operator: '=', + value: 'Not This Title', + }).matchesItem(item), + ).toEqual(true) + }) + + it('nonmatching basic operator', () => { + expect( + notPredicateFromArguments({ + keypath: 'title', + operator: '=', + value: 'Hello', + }).matchesItem(item), + ).toEqual(false) + }) + + it('matching compound', () => { + expect( + new CompoundPredicate('and', [ + new NotPredicate(new IncludesPredicate('tags', new Predicate('title', '=', 'boo'))), + new IncludesPredicate('tags', new Predicate('title', '=', 'foo')), + ]).matchesItem(item), + ).toEqual(true) + }) + + it('matching compound includes', () => { + const andPredicate = new CompoundPredicate('and', [ + predicateFromArguments('title', 'startsWith', 'H'), + includesPredicateFromArguments('tags', { + keypath: 'title', + operator: '=', + value: 'falsify', + }), + ]) + expect(new NotPredicate(andPredicate).matchesItem(item)).toEqual(true) + }) + + it('nonmatching compound includes', () => { + expect( + new NotPredicate( + new CompoundPredicate('and', [ + new Predicate('title', 'startsWith', 'H'), + new IncludesPredicate('tags', new Predicate('title', '=', 'foo')), + ]), + ).matchesItem(item), + ).toEqual(false) + }) + + it('nonmatching compound or', () => { + expect( + new NotPredicate( + new CompoundPredicate('or', [ + new Predicate('title', 'startsWith', 'H'), + new IncludesPredicate('tags', new Predicate('title', '=', 'falsify')), + ]), + ).matchesItem(item), + ).toEqual(false) + }) + + it('matching compound or', () => { + expect( + new NotPredicate( + new CompoundPredicate('or', [ + new Predicate('title', 'startsWith', 'Z'), + new IncludesPredicate('tags', new Predicate('title', '=', 'falsify')), + ]), + ).matchesItem(item), + ).toEqual(true) + }) + }) + + describe('regex', () => { + it('matching', () => { + const item = createNote(createNoteContent('abc')) + const onlyLetters = new Predicate('title', 'matches', '^[a-zA-Z]+$') + expect(onlyLetters.matchesItem(item)).toEqual(true) + }) + + it('nonmatching', () => { + const item = createNote(createNoteContent('123')) + const onlyLetters = new Predicate('title', 'matches', '^[a-zA-Z]+$') + expect(onlyLetters.matchesItem(item)).toEqual(false) + }) + }) + + describe('deep recursion', () => { + let item: Note + const title = 'Hello' + beforeEach(() => { + item = createNote(createNoteContent(title)) + }) + + it('matching', () => { + expect( + new CompoundPredicate('and', [ + new Predicate('title', '=', 'Hello'), + new CompoundPredicate('or', [ + new Predicate('title', '=', 'Wrong'), + new Predicate('title', '=', 'Wrong again'), + new Predicate('title', '=', 'Hello'), + ]), + ]).matchesItem(item), + ).toEqual(true) + }) + + it('nonmatching', () => { + expect( + new CompoundPredicate('and', [ + new Predicate('title', '=', 'Hello'), + new CompoundPredicate('or', [ + new Predicate('title', '=', 'Wrong'), + new Predicate('title', '=', 'Wrong again'), + new Predicate('title', '=', 'All wrong'), + ]), + ]).matchesItem(item), + ).toEqual(false) + }) + }) + + describe('inequality operator', () => { + let item: Item + const body = 'Hello' + const numbers = ['1', '2', '3'] + + beforeEach(() => { + item = createItem({ body, numbers }) + }) + + it('matching', () => { + expect(new Predicate('body', '!=', 'NotBody').matchesItem(item)).toEqual(true) + }) + + it('nonmatching', () => { + expect(new Predicate('body', '!=', body).matchesItem(item)).toEqual(false) + }) + + it('matching array', () => { + expect(new Predicate('numbers', '!=', ['1']).matchesItem(item)).toEqual(true) + }) + + it('nonmatching array', () => { + expect(new Predicate('numbers', '!=', ['1', '2', '3']).matchesItem(item)).toEqual(false) + }) + }) + + describe('equals operator', () => { + let item: Item + const body = 'Hello' + const numbers = ['1', '2', '3'] + + beforeEach(() => { + item = createItem({ body, numbers }) + }) + + it('matching', () => { + expect(new Predicate('body', '=', body).matchesItem(item)).toEqual(true) + }) + + it('nonmatching', () => { + expect(new Predicate('body', '=', 'NotBody').matchesItem(item)).toEqual(false) + }) + + it('false and undefined should be equivalent', () => { + expect(new Predicate('undefinedProperty', '=', false).matchesItem(item)).toEqual(true) + }) + + it('nonmatching array', () => { + expect(new Predicate('numbers', '=', ['1']).matchesItem(item)).toEqual(false) + }) + + it('matching array', () => { + expect(new Predicate('numbers', '=', ['1', '2', '3']).matchesItem(item)).toEqual(true) + }) + + it('nested keypath', () => { + expect(new Predicate('numbers.length', '=', numbers.length).matchesItem(item)).toEqual(true) + }) + }) + + describe('date comparison', () => { + let item: Item + const date = new Date() + + beforeEach(() => { + item = createItem({}, date) + }) + + it('nonmatching date value', () => { + const date = new Date() + date.setSeconds(date.getSeconds() + 1) + const predicate = new Predicate('updated_at', '>', date) + expect(predicate.matchesItem(item)).toEqual(false) + }) + + it('matching date value', () => { + const date = new Date() + date.setSeconds(date.getSeconds() + 1) + const predicate = new Predicate('updated_at', '<', date) + expect(predicate.matchesItem(item)).toEqual(true) + }) + + it('matching days ago value', () => { + expect(new Predicate('updated_at', '>', '30.days.ago').matchesItem(item)).toEqual(true) + }) + + it('nonmatching days ago value', () => { + expect(new Predicate('updated_at', '<', '30.days.ago').matchesItem(item)).toEqual(false) + }) + + it('hours ago value', () => { + expect(new Predicate('updated_at', '>', '1.hours.ago').matchesItem(item)).toEqual(true) + }) + }) + + describe('nonexistent properties', () => { + let item: Item + + beforeEach(() => { + item = createItem({}) + }) + + it('nested keypath', () => { + expect(new Predicate('foobar.length', '=', 0).matchesItem(item)).toEqual(false) + }) + + it('inequality operator', () => { + expect(new Predicate('foobar', '!=', 'NotFoo').matchesItem(item)).toEqual(true) + }) + + it('equals operator', () => { + expect(new Predicate('foobar', '=', 'NotFoo').matchesItem(item)).toEqual(false) + }) + + it('less than operator', () => { + expect(new Predicate('foobar', '<', 3).matchesItem(item)).toEqual(false) + }) + + it('greater than operator', () => { + expect(new Predicate('foobar', '>', 3).matchesItem(item)).toEqual(false) + }) + + it('less than or equal to operator', () => { + expect(new Predicate('foobar', '<=', 3).matchesItem(item)).toEqual(false) + }) + + it('includes operator', () => { + expect(new Predicate('foobar', 'includes', 3).matchesItem(item)).toEqual(false) + }) + }) + + describe('toJson', () => { + it('basic predicate', () => { + const predicate = new Predicate('title', 'startsWith', 'H') + const json = predicate.toJson() + + expect(json).toStrictEqual({ + keypath: 'title', + operator: 'startsWith', + value: 'H', + }) + }) + + it('compound and', () => { + const predicate = new CompoundPredicate('and', [ + new Predicate('title', 'startsWith', 'H'), + new Predicate('title', '=', 'Hello'), + ]) + const json = predicate.toJson() + + expect(json).toStrictEqual({ + operator: 'and', + value: [ + { + keypath: 'title', + operator: 'startsWith', + value: 'H', + }, + { + keypath: 'title', + operator: '=', + value: 'Hello', + }, + ], + }) + }) + + it('not', () => { + const predicate = new NotPredicate(new Predicate('title', 'startsWith', 'H')) + const json = predicate.toJson() + + expect(json).toStrictEqual({ + operator: 'not', + value: { + keypath: 'title', + operator: 'startsWith', + value: 'H', + }, + }) + }) + + it('not compound', () => { + const predicate = new NotPredicate( + new CompoundPredicate('or', [ + new Predicate('title', 'startsWith', 'H'), + new IncludesPredicate('tags', new Predicate('title', '=', 'falsify')), + ]), + ) + + const json = predicate.toJson() + + expect(json).toStrictEqual({ + operator: 'not', + value: { + operator: 'or', + value: [ + { + keypath: 'title', + operator: 'startsWith', + value: 'H', + }, + { + keypath: 'tags', + operator: 'includes', + value: { + keypath: 'title', + operator: '=', + value: 'falsify', + }, + }, + ], + }, + }) + }) + }) + + describe('generators', () => { + it('includes predicate', () => { + const json = ['B-tags', 'tags', 'includes', ['title', 'startsWith', 'b']] + const predicate = predicateFromDSLString('!' + JSON.stringify(json)) as IncludesPredicate + + expect(predicate).toBeInstanceOf(IncludesPredicate) + expect(predicate.predicate).toBeInstanceOf(Predicate) + expect((predicate.predicate as Predicate).keypath).toEqual('title') + expect((predicate.predicate as Predicate).operator).toEqual('startsWith') + }) + + it('includes string should be mapped to normal predicate', () => { + const json = ['TODO', 'title', 'includes', 'TODO'] + const predicate = predicateFromDSLString('!' + JSON.stringify(json)) as Predicate + + expect(predicate).toBeInstanceOf(Predicate) + expect(predicate.keypath).toEqual('title') + expect(predicate.operator).toEqual('includes') + }) + + it('complex compound and', () => { + const json = [ + 'label', + 'ignored_keypath', + 'and', + [ + ['', 'not', ['tags', 'includes', ['title', '=', 'boo']]], + ['tags', 'includes', ['title', '=', 'foo']], + ], + ] + + const predicate = predicateFromDSLString('!' + JSON.stringify(json)) as CompoundPredicate + + expect(predicate).toBeInstanceOf(CompoundPredicate) + + expect(predicate.predicates).toHaveLength(2) + + const notPredicate = predicate.predicates[0] as NotPredicate + expect(notPredicate).toBeInstanceOf(NotPredicate) + + const includesPredicate = predicate.predicates[1] + expect(includesPredicate).toBeInstanceOf(IncludesPredicate) + + expect(notPredicate.predicate).toBeInstanceOf(IncludesPredicate) + expect((notPredicate.predicate as IncludesPredicate).predicate).toBeInstanceOf(Predicate) + }) + + it('nested compound or', () => { + const json = [ + 'label', + 'ignored_keypath', + 'and', + [ + ['title', '=', 'Hello'], + [ + 'this_field_ignored', + 'or', + [ + ['title', '=', 'Wrong'], + ['title', '=', 'Wrong again'], + ['title', '=', 'All wrong'], + ], + ], + ], + ] + + const predicate = predicateFromDSLString('!' + JSON.stringify(json)) as CompoundPredicate + + expect(predicate).toBeInstanceOf(CompoundPredicate) + + expect(predicate.predicates).toHaveLength(2) + + expect(predicate.predicates[0]).toBeInstanceOf(Predicate) + + const orPredicate = predicate.predicates[1] as CompoundPredicate + expect(orPredicate).toBeInstanceOf(CompoundPredicate) + expect(orPredicate.predicates).toHaveLength(3) + expect(orPredicate.operator).toEqual('or') + + for (const subPredicate of orPredicate.predicates) { + expect(subPredicate).toBeInstanceOf(Predicate) + } + }) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Predicate/Predicate.ts b/packages/models/src/Domain/Runtime/Predicate/Predicate.ts new file mode 100644 index 000000000..6da4d7ca7 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/Predicate.ts @@ -0,0 +1,49 @@ +import { + PredicateTarget, + PredicateInterface, + PredicateJsonForm, + PredicateOperator, + PrimitiveOperand, + StringKey, + SureValue, +} from './Interface' +import { valueMatchesTargetValue } from './Operator' + +/** + * A local-only construct that defines a built query that + * can be used to dynamically search items. + */ +export class Predicate implements PredicateInterface { + constructor( + public readonly keypath: StringKey, + public readonly operator: PredicateOperator, + public readonly targetValue: SureValue, + ) { + if (this.targetValue === 'true' || this.targetValue === 'false') { + this.targetValue = JSON.parse(this.targetValue) + } + } + + keypathIncludesString(verb: string): boolean { + return (this.keypath as string).includes(verb) + } + + matchesItem(item: T): boolean { + const keyPathComponents = this.keypath.split('.') as StringKey[] + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const valueAtKeyPath: PrimitiveOperand = keyPathComponents.reduce((previous, current) => { + return previous && previous[current] + }, item) + + return valueMatchesTargetValue(valueAtKeyPath, this.operator, this.targetValue) + } + + toJson(): PredicateJsonForm { + return { + keypath: this.keypath, + operator: this.operator, + value: this.targetValue, + } + } +} diff --git a/packages/models/src/Domain/Runtime/Predicate/Utils.ts b/packages/models/src/Domain/Runtime/Predicate/Utils.ts new file mode 100644 index 000000000..5c6b737fb --- /dev/null +++ b/packages/models/src/Domain/Runtime/Predicate/Utils.ts @@ -0,0 +1,15 @@ +/** + * Predicate date strings are of form "x.days.ago" or "x.hours.ago" + */ +export function dateFromDSLDateString(string: string): Date { + const comps = string.split('.') + const unit = comps[1] + const date = new Date() + const offset = parseInt(comps[0]) + if (unit === 'days') { + date.setDate(date.getDate() - offset) + } else if (unit === 'hours') { + date.setHours(date.getHours() - offset) + } + return date +} diff --git a/packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtension.ts b/packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtension.ts new file mode 100644 index 000000000..6212464b4 --- /dev/null +++ b/packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtension.ts @@ -0,0 +1,72 @@ +import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem' +import { ThirdPartyFeatureDescription } from '@standardnotes/features' +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { HistoryEntryInterface } from '../../Runtime/History/HistoryEntryInterface' +import { Action } from './Types' +import { ComponentPackageInfo } from '../Component/PackageInfo' + +export interface ActionExtensionInterface { + actions: Action[] + deprecation?: string + description: string + hosted_url?: string + name: string + package_info: ComponentPackageInfo + supported_types: string[] + url: string +} + +export type ActionExtensionContent = ActionExtensionInterface & ItemContent + +/** + * Related to the SNActionsService and the local Action model. + */ +export class SNActionsExtension extends DecryptedItem { + public readonly actions: Action[] = [] + public readonly description: string + public readonly url: string + public readonly supported_types: string[] + public readonly deprecation?: string + public readonly name: string + public readonly package_info: ComponentPackageInfo + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + this.name = payload.content.name || '' + this.description = payload.content.description || '' + this.url = payload.content.hosted_url || payload.content.url + this.supported_types = payload.content.supported_types + this.package_info = this.payload.content.package_info || {} + this.deprecation = payload.content.deprecation + this.actions = payload.content.actions + } + + public get displayName(): string { + return this.name + } + + public get thirdPartyPackageInfo(): ThirdPartyFeatureDescription { + return this.package_info as ThirdPartyFeatureDescription + } + + public get isListedExtension(): boolean { + return (this.package_info.identifier as string) === 'org.standardnotes.listed' + } + + actionsWithContextForItem(item: DecryptedItemInterface): Action[] { + return this.actions.filter((action) => { + return action.context === item.content_type || action.context === 'Item' + }) + } + + /** Do not duplicate. Always keep original */ + override strategyWhenConflictingWithItem( + _item: DecryptedItemInterface, + _previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + return ConflictStrategy.KeepBase + } +} diff --git a/packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtensionMutator.ts b/packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtensionMutator.ts new file mode 100644 index 000000000..c730d3f90 --- /dev/null +++ b/packages/models/src/Domain/Syncable/ActionsExtension/ActionsExtensionMutator.ts @@ -0,0 +1,21 @@ +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' +import { ActionExtensionContent } from './ActionsExtension' +import { Action } from './Types' + +export class ActionsExtensionMutator extends DecryptedItemMutator { + set description(description: string) { + this.mutableContent.description = description + } + + set supported_types(supported_types: string[]) { + this.mutableContent.supported_types = supported_types + } + + set actions(actions: Action[]) { + this.mutableContent.actions = actions + } + + set deprecation(deprecation: string | undefined) { + this.mutableContent.deprecation = deprecation + } +} diff --git a/packages/models/src/Domain/Syncable/ActionsExtension/Types.ts b/packages/models/src/Domain/Syncable/ActionsExtension/Types.ts new file mode 100644 index 000000000..bfdf8907b --- /dev/null +++ b/packages/models/src/Domain/Syncable/ActionsExtension/Types.ts @@ -0,0 +1,25 @@ +export enum ActionAccessType { + Encrypted = 'encrypted', + Decrypted = 'decrypted', +} + +export enum ActionVerb { + Get = 'get', + Render = 'render', + Show = 'show', + Post = 'post', + Nested = 'nested', +} + +export type Action = { + label: string + desc: string + running?: boolean + error?: boolean + lastExecuted?: Date + context?: string + verb: ActionVerb + url: string + access_type: ActionAccessType + subactions?: Action[] +} diff --git a/packages/models/src/Domain/Syncable/ActionsExtension/index.ts b/packages/models/src/Domain/Syncable/ActionsExtension/index.ts new file mode 100644 index 000000000..eed2bc3f0 --- /dev/null +++ b/packages/models/src/Domain/Syncable/ActionsExtension/index.ts @@ -0,0 +1,3 @@ +export * from './ActionsExtension' +export * from './ActionsExtensionMutator' +export * from './Types' diff --git a/packages/models/src/Domain/Syncable/Component/Component.spec.ts b/packages/models/src/Domain/Syncable/Component/Component.spec.ts new file mode 100644 index 000000000..68ccc5399 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Component/Component.spec.ts @@ -0,0 +1,49 @@ +import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource' +import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload' +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { SNComponent } from './Component' +import { ComponentContent } from './ComponentContent' +import { PayloadTimestampDefaults } from '../../Abstract/Payload' + +describe('component model', () => { + it('valid hosted url should ignore url', () => { + const component = new SNComponent( + new DecryptedPayload( + { + uuid: String(Math.random()), + content_type: ContentType.Component, + content: FillItemContent({ + url: 'http://foo.com', + hosted_url: 'http://bar.com', + } as ComponentContent), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) + + expect(component.hasValidHostedUrl()).toBe(true) + expect(component.hosted_url).toBe('http://bar.com') + }) + + it('invalid hosted url should fallback to url', () => { + const component = new SNComponent( + new DecryptedPayload( + { + uuid: String(Math.random()), + content_type: ContentType.Component, + content: FillItemContent({ + url: 'http://foo.com', + hosted_url: '#{foo.zoo}', + } as ComponentContent), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) + + expect(component.hasValidHostedUrl()).toBe(true) + expect(component.hosted_url).toBe('http://foo.com') + }) +}) diff --git a/packages/models/src/Domain/Syncable/Component/Component.ts b/packages/models/src/Domain/Syncable/Component/Component.ts new file mode 100644 index 000000000..595043d14 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Component/Component.ts @@ -0,0 +1,189 @@ +import { isValidUrl } from '@standardnotes/utils' +import { ContentType, Uuid } from '@standardnotes/common' +import { + FeatureIdentifier, + ThirdPartyFeatureDescription, + ComponentArea, + ComponentFlag, + ComponentPermission, + FindNativeFeature, +} from '@standardnotes/features' +import { AppDataField } from '../../Abstract/Item/Types/AppDataField' +import { ComponentContent, ComponentInterface } from './ComponentContent' +import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy' +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { HistoryEntryInterface } from '../../Runtime/History' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { Predicate } from '../../Runtime/Predicate/Predicate' +import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' +import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem' +import { ComponentPackageInfo } from './PackageInfo' + +export const isComponent = (x: ItemInterface): x is SNComponent => x.content_type === ContentType.Component + +export const isComponentOrTheme = (x: ItemInterface): x is SNComponent => + x.content_type === ContentType.Component || x.content_type === ContentType.Theme + +/** + * Components are mostly iframe based extensions that communicate with the SN parent + * via the postMessage API. However, a theme can also be a component, which is activated + * only by its url. + */ +export class SNComponent extends DecryptedItem implements ComponentInterface { + public readonly componentData: Record + /** Items that have requested a component to be disabled in its context */ + public readonly disassociatedItemIds: string[] + /** Items that have requested a component to be enabled in its context */ + public readonly associatedItemIds: string[] + public readonly local_url?: string + public readonly hosted_url?: string + public readonly offlineOnly: boolean + public readonly name: string + public readonly autoupdateDisabled: boolean + public readonly package_info: ComponentPackageInfo + public readonly area: ComponentArea + public readonly permissions: ComponentPermission[] = [] + public readonly valid_until: Date + public readonly active: boolean + public readonly legacy_url?: string + public readonly isMobileDefault: boolean + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + /** Custom data that a component can store in itself */ + this.componentData = this.payload.content.componentData || {} + + if (payload.content.hosted_url && isValidUrl(payload.content.hosted_url)) { + this.hosted_url = payload.content.hosted_url + } else if (payload.content.url && isValidUrl(payload.content.url)) { + this.hosted_url = payload.content.url + } else if (payload.content.legacy_url && isValidUrl(payload.content.legacy_url)) { + this.hosted_url = payload.content.legacy_url + } + this.local_url = payload.content.local_url + + this.valid_until = new Date(payload.content.valid_until || 0) + this.offlineOnly = payload.content.offlineOnly + this.name = payload.content.name + this.area = payload.content.area + this.package_info = payload.content.package_info || {} + this.permissions = payload.content.permissions || [] + this.active = payload.content.active + this.autoupdateDisabled = payload.content.autoupdateDisabled + this.disassociatedItemIds = payload.content.disassociatedItemIds || [] + this.associatedItemIds = payload.content.associatedItemIds || [] + this.isMobileDefault = payload.content.isMobileDefault + /** + * @legacy + * We don't want to set this.url directly, as we'd like to phase it out. + * If the content.url exists, we'll transfer it to legacy_url. We'll only + * need to set this if content.hosted_url is blank, otherwise, + * hosted_url is the url replacement. + */ + this.legacy_url = !payload.content.hosted_url ? payload.content.url : undefined + } + + /** Do not duplicate components under most circumstances. Always keep original */ + public override strategyWhenConflictingWithItem( + _item: DecryptedItemInterface, + _previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + return ConflictStrategy.KeepBase + } + + override get isSingleton(): boolean { + return true + } + + public get displayName(): string { + return FindNativeFeature(this.identifier)?.name || this.name + } + + public override singletonPredicate(): Predicate { + const uniqueIdentifierPredicate = new Predicate('identifier', '=', this.identifier) + return uniqueIdentifierPredicate + } + + public isEditor(): boolean { + return this.area === ComponentArea.Editor + } + + public isTheme(): boolean { + return this.content_type === ContentType.Theme || this.area === ComponentArea.Themes + } + + public isDefaultEditor(): boolean { + return this.getAppDomainValue(AppDataField.DefaultEditor) === true + } + + public getLastSize(): unknown { + return this.getAppDomainValue(AppDataField.LastSize) + } + + /** + * The key used to look up data that this component may have saved to an item. + * This data will be stored on the item using this key. + */ + public getClientDataKey(): string { + if (this.legacy_url) { + return this.legacy_url + } else { + return this.uuid + } + } + + public hasValidHostedUrl(): boolean { + return (this.hosted_url || this.legacy_url) != undefined + } + + public override contentKeysToIgnoreWhenCheckingEquality(): (keyof ItemContent)[] { + const componentKeys: (keyof ComponentContent)[] = ['active', 'disassociatedItemIds', 'associatedItemIds'] + + const superKeys = super.contentKeysToIgnoreWhenCheckingEquality() + return [...componentKeys, ...superKeys] as (keyof ItemContent)[] + } + + /** + * An associative component depends on being explicitly activated for a + * given item, compared to a dissaciative component, which is enabled by + * default in areas unrelated to a certain item. + */ + public static associativeAreas(): ComponentArea[] { + return [ComponentArea.Editor] + } + + public isAssociative(): boolean { + return SNComponent.associativeAreas().includes(this.area) + } + + public isExplicitlyEnabledForItem(uuid: Uuid): boolean { + return this.associatedItemIds.indexOf(uuid) !== -1 + } + + public isExplicitlyDisabledForItem(uuid: Uuid): boolean { + return this.disassociatedItemIds.indexOf(uuid) !== -1 + } + + public get isExpired(): boolean { + return this.valid_until.getTime() > 0 && this.valid_until <= new Date() + } + + public get identifier(): FeatureIdentifier { + return this.package_info.identifier + } + + public get thirdPartyPackageInfo(): ThirdPartyFeatureDescription { + return this.package_info as ThirdPartyFeatureDescription + } + + public get isDeprecated(): boolean { + let flags: string[] = this.package_info.flags ?? [] + flags = flags.map((flag: string) => flag.toLowerCase()) + return flags.includes(ComponentFlag.Deprecated) + } + + public get deprecationMessage(): string | undefined { + return this.package_info.deprecation_message + } +} diff --git a/packages/models/src/Domain/Syncable/Component/ComponentContent.ts b/packages/models/src/Domain/Syncable/Component/ComponentContent.ts new file mode 100644 index 000000000..9e6787e42 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Component/ComponentContent.ts @@ -0,0 +1,36 @@ +import { ComponentArea, ComponentPermission } from '@standardnotes/features' +import { Uuid } from '@standardnotes/common' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { ComponentPackageInfo } from './PackageInfo' + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export interface ComponentInterface { + componentData: Record + + /** Items that have requested a component to be disabled in its context */ + disassociatedItemIds: string[] + + /** Items that have requested a component to be enabled in its context */ + associatedItemIds: string[] + + local_url?: string + hosted_url?: string + + /** @deprecated */ + url?: string + + offlineOnly: boolean + name: string + autoupdateDisabled: boolean + package_info: ComponentPackageInfo + area: ComponentArea + permissions: ComponentPermission[] + valid_until: Date | number + active: boolean + legacy_url?: string + isMobileDefault: boolean + isDeprecated: boolean + isExplicitlyEnabledForItem(uuid: Uuid): boolean +} + +export type ComponentContent = ComponentInterface & ItemContent diff --git a/packages/models/src/Domain/Syncable/Component/ComponentMutator.ts b/packages/models/src/Domain/Syncable/Component/ComponentMutator.ts new file mode 100644 index 000000000..9b24c8aac --- /dev/null +++ b/packages/models/src/Domain/Syncable/Component/ComponentMutator.ts @@ -0,0 +1,76 @@ +import { addIfUnique, removeFromArray } from '@standardnotes/utils' +import { Uuid } from '@standardnotes/common' +import { ComponentPermission, FeatureDescription } from '@standardnotes/features' +import { AppDataField } from '../../Abstract/Item/Types/AppDataField' +import { ComponentContent } from './ComponentContent' +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' + +export class ComponentMutator extends DecryptedItemMutator { + set active(active: boolean) { + this.mutableContent.active = active + } + + set isMobileDefault(isMobileDefault: boolean) { + this.mutableContent.isMobileDefault = isMobileDefault + } + + set defaultEditor(defaultEditor: boolean) { + this.setAppDataItem(AppDataField.DefaultEditor, defaultEditor) + } + + set componentData(componentData: Record) { + this.mutableContent.componentData = componentData + } + + set package_info(package_info: FeatureDescription) { + this.mutableContent.package_info = package_info + } + + set local_url(local_url: string) { + this.mutableContent.local_url = local_url + } + + set hosted_url(hosted_url: string) { + this.mutableContent.hosted_url = hosted_url + } + + set valid_until(valid_until: Date) { + this.mutableContent.valid_until = valid_until + } + + set permissions(permissions: ComponentPermission[]) { + this.mutableContent.permissions = permissions + } + + set name(name: string) { + this.mutableContent.name = name + } + + set offlineOnly(offlineOnly: boolean) { + this.mutableContent.offlineOnly = offlineOnly + } + + public associateWithItem(uuid: Uuid): void { + const associated = this.mutableContent.associatedItemIds || [] + addIfUnique(associated, uuid) + this.mutableContent.associatedItemIds = associated + } + + public disassociateWithItem(uuid: Uuid): void { + const disassociated = this.mutableContent.disassociatedItemIds || [] + addIfUnique(disassociated, uuid) + this.mutableContent.disassociatedItemIds = disassociated + } + + public removeAssociatedItemId(uuid: Uuid): void { + removeFromArray(this.mutableContent.associatedItemIds || [], uuid) + } + + public removeDisassociatedItemId(uuid: Uuid): void { + removeFromArray(this.mutableContent.disassociatedItemIds || [], uuid) + } + + public setLastSize(size: string): void { + this.setAppDataItem(AppDataField.LastSize, size) + } +} diff --git a/packages/models/src/Domain/Syncable/Component/PackageInfo.ts b/packages/models/src/Domain/Syncable/Component/PackageInfo.ts new file mode 100644 index 000000000..6f9b1820a --- /dev/null +++ b/packages/models/src/Domain/Syncable/Component/PackageInfo.ts @@ -0,0 +1,8 @@ +import { FeatureDescription } from '@standardnotes/features' + +type ThirdPartyPackageInfo = { + version: string + download_url?: string +} + +export type ComponentPackageInfo = FeatureDescription & Partial diff --git a/packages/models/src/Domain/Syncable/Component/index.ts b/packages/models/src/Domain/Syncable/Component/index.ts new file mode 100644 index 000000000..6cbb1cc10 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Component/index.ts @@ -0,0 +1,3 @@ +export * from './Component' +export * from './ComponentMutator' +export * from './ComponentContent' diff --git a/packages/models/src/Domain/Syncable/Editor/Editor.ts b/packages/models/src/Domain/Syncable/Editor/Editor.ts new file mode 100644 index 000000000..9f748d9f5 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Editor/Editor.ts @@ -0,0 +1,35 @@ +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { SNNote } from '../Note/Note' + +interface EditorContent extends ItemContent { + notes: SNNote[] + data: Record + url: string + name: string + default: boolean + systemEditor: boolean +} + +/** + * @deprecated + * Editor objects are depracated in favor of SNComponent objects + */ +export class SNEditor extends DecryptedItem { + public readonly notes: SNNote[] = [] + public readonly data: Record = {} + public readonly url: string + public readonly name: string + public readonly isDefault: boolean + public readonly systemEditor: boolean + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + this.url = payload.content.url + this.name = payload.content.name + this.data = payload.content.data || {} + this.isDefault = payload.content.default + this.systemEditor = payload.content.systemEditor + } +} diff --git a/packages/models/src/Domain/Syncable/Editor/index.ts b/packages/models/src/Domain/Syncable/Editor/index.ts new file mode 100644 index 000000000..740c04420 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Editor/index.ts @@ -0,0 +1 @@ +export * from './Editor' diff --git a/packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepo.ts b/packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepo.ts new file mode 100644 index 000000000..5db7cc04b --- /dev/null +++ b/packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepo.ts @@ -0,0 +1,33 @@ +import { useBoolean } from '@standardnotes/utils' +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { ItemContent } from '../../Abstract/Content/ItemContent' + +export interface FeatureRepoContent extends ItemContent { + migratedToUserSetting?: boolean + migratedToOfflineEntitlements?: boolean + offlineFeaturesUrl?: string + offlineKey?: string + url?: string +} + +export class SNFeatureRepo extends DecryptedItem { + public get migratedToUserSetting(): boolean { + return useBoolean(this.payload.content.migratedToUserSetting, false) + } + + public get migratedToOfflineEntitlements(): boolean { + return useBoolean(this.payload.content.migratedToOfflineEntitlements, false) + } + + public get onlineUrl(): string | undefined { + return this.payload.content.url + } + + get offlineFeaturesUrl(): string | undefined { + return this.payload.content.offlineFeaturesUrl + } + + get offlineKey(): string | undefined { + return this.payload.content.offlineKey + } +} diff --git a/packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepoMutator.ts b/packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepoMutator.ts new file mode 100644 index 000000000..a5ce25545 --- /dev/null +++ b/packages/models/src/Domain/Syncable/FeatureRepo/FeatureRepoMutator.ts @@ -0,0 +1,20 @@ +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' +import { FeatureRepoContent } from './FeatureRepo' + +export class FeatureRepoMutator extends DecryptedItemMutator { + set migratedToUserSetting(migratedToUserSetting: boolean) { + this.mutableContent.migratedToUserSetting = migratedToUserSetting + } + + set migratedToOfflineEntitlements(migratedToOfflineEntitlements: boolean) { + this.mutableContent.migratedToOfflineEntitlements = migratedToOfflineEntitlements + } + + set offlineFeaturesUrl(offlineFeaturesUrl: string) { + this.mutableContent.offlineFeaturesUrl = offlineFeaturesUrl + } + + set offlineKey(offlineKey: string) { + this.mutableContent.offlineKey = offlineKey + } +} diff --git a/packages/models/src/Domain/Syncable/FeatureRepo/index.ts b/packages/models/src/Domain/Syncable/FeatureRepo/index.ts new file mode 100644 index 000000000..655bd8dd9 --- /dev/null +++ b/packages/models/src/Domain/Syncable/FeatureRepo/index.ts @@ -0,0 +1,2 @@ +export * from './FeatureRepo' +export * from './FeatureRepoMutator' diff --git a/packages/models/src/Domain/Syncable/File/File.spec.ts b/packages/models/src/Domain/Syncable/File/File.spec.ts new file mode 100644 index 000000000..466f6b019 --- /dev/null +++ b/packages/models/src/Domain/Syncable/File/File.spec.ts @@ -0,0 +1,75 @@ +import { ConflictStrategy } from './../../Abstract/Item/Types/ConflictStrategy' +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload' +import { FileContent, FileItem } from './File' +import { UuidGenerator } from '@standardnotes/utils' + +UuidGenerator.SetGenerator(() => String(Math.random())) + +describe('file', () => { + const createFile = (content: Partial = {}): FileItem => { + return new FileItem( + new DecryptedPayload({ + uuid: '123', + content_type: ContentType.File, + content: FillItemContent({ + name: 'name.png', + key: 'secret', + remoteIdentifier: 'A', + encryptionHeader: 'header', + encryptedChunkSizes: [1, 2, 3], + ...content, + }), + dirty: true, + ...PayloadTimestampDefaults(), + }), + ) + } + + const copyFile = (file: FileItem, override: Partial = {}): FileItem => { + return new FileItem( + file.payload.copy({ + content: { + ...file.content, + ...override, + } as FileContent, + }), + ) + } + + it('should not copy on name conflict', () => { + const file = createFile({ name: 'file.png' }) + const conflictedFile = copyFile(file, { name: 'different.png' }) + + expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBase) + }) + + it('should copy on key conflict', () => { + const file = createFile({ name: 'file.png' }) + const conflictedFile = copyFile(file, { key: 'different-secret' }) + + expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply) + }) + + it('should copy on header conflict', () => { + const file = createFile({ name: 'file.png' }) + const conflictedFile = copyFile(file, { encryptionHeader: 'different-header' }) + + expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply) + }) + + it('should copy on identifier conflict', () => { + const file = createFile({ name: 'file.png' }) + const conflictedFile = copyFile(file, { remoteIdentifier: 'different-identifier' }) + + expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply) + }) + + it('should copy on chunk sizes conflict', () => { + const file = createFile({ name: 'file.png' }) + const conflictedFile = copyFile(file, { encryptedChunkSizes: [10, 9, 8] }) + + expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply) + }) +}) diff --git a/packages/models/src/Domain/Syncable/File/File.ts b/packages/models/src/Domain/Syncable/File/File.ts new file mode 100644 index 000000000..0a79acc81 --- /dev/null +++ b/packages/models/src/Domain/Syncable/File/File.ts @@ -0,0 +1,85 @@ +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { FileMetadata } from './FileMetadata' +import { FileProtocolV1 } from './FileProtocolV1' +import { SortableItem } from '../../Runtime/Collection/CollectionSort' +import { ConflictStrategy } from '../../Abstract/Item' + +type EncryptedBytesLength = number +type DecryptedBytesLength = number + +interface SizesDeprecatedDueToAmbiguousNaming { + size?: DecryptedBytesLength + chunkSizes?: EncryptedBytesLength[] +} + +interface Sizes { + decryptedSize: DecryptedBytesLength + encryptedChunkSizes: EncryptedBytesLength[] +} + +interface FileContentWithoutSize { + remoteIdentifier: string + name: string + key: string + encryptionHeader: string + mimeType: string +} + +export type FileContentSpecialized = FileContentWithoutSize & FileMetadata & SizesDeprecatedDueToAmbiguousNaming & Sizes + +export type FileContent = FileContentSpecialized & ItemContent + +export class FileItem + extends DecryptedItem + implements FileContentWithoutSize, Sizes, FileProtocolV1, FileMetadata, SortableItem +{ + public readonly remoteIdentifier: string + public readonly name: string + public readonly key: string + public readonly encryptionHeader: string + public readonly mimeType: string + + public readonly decryptedSize: DecryptedBytesLength + public readonly encryptedChunkSizes: EncryptedBytesLength[] + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + this.remoteIdentifier = this.content.remoteIdentifier + this.name = this.content.name + this.key = this.content.key + + if (this.content.size && this.content.chunkSizes) { + this.decryptedSize = this.content.size + this.encryptedChunkSizes = this.content.chunkSizes + } else { + this.decryptedSize = this.content.decryptedSize + this.encryptedChunkSizes = this.content.encryptedChunkSizes + } + + this.encryptionHeader = this.content.encryptionHeader + this.mimeType = this.content.mimeType + } + + public override strategyWhenConflictingWithItem(item: FileItem): ConflictStrategy { + if ( + item.key !== this.key || + item.encryptionHeader !== this.encryptionHeader || + item.remoteIdentifier !== this.remoteIdentifier || + JSON.stringify(item.encryptedChunkSizes) !== JSON.stringify(this.encryptedChunkSizes) + ) { + return ConflictStrategy.KeepBaseDuplicateApply + } + + return ConflictStrategy.KeepBase + } + + public get encryptedSize(): number { + return this.encryptedChunkSizes.reduce((total, chunk) => total + chunk, 0) + } + + public get title(): string { + return this.name + } +} diff --git a/packages/models/src/Domain/Syncable/File/FileMetadata.ts b/packages/models/src/Domain/Syncable/File/FileMetadata.ts new file mode 100644 index 000000000..11a55799a --- /dev/null +++ b/packages/models/src/Domain/Syncable/File/FileMetadata.ts @@ -0,0 +1,4 @@ +export interface FileMetadata { + name: string + mimeType: string +} diff --git a/packages/models/src/Domain/Syncable/File/FileMutator.ts b/packages/models/src/Domain/Syncable/File/FileMutator.ts new file mode 100644 index 000000000..93b834845 --- /dev/null +++ b/packages/models/src/Domain/Syncable/File/FileMutator.ts @@ -0,0 +1,33 @@ +import { ContentType } from '@standardnotes/common' +import { SNNote } from '../Note/Note' +import { FileContent } from './File' +import { FileToNoteReference } from '../../Abstract/Reference/FileToNoteReference' +import { ContenteReferenceType } from '../../Abstract/Reference/ContenteReferenceType' +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' + +export class FileMutator extends DecryptedItemMutator { + set name(newName: string) { + this.mutableContent.name = newName + } + + set encryptionHeader(encryptionHeader: string) { + this.mutableContent.encryptionHeader = encryptionHeader + } + + public addNote(note: SNNote): void { + const reference: FileToNoteReference = { + reference_type: ContenteReferenceType.FileToNote, + content_type: ContentType.Note, + uuid: note.uuid, + } + + const references = this.mutableContent.references || [] + references.push(reference) + this.mutableContent.references = references + } + + public removeNote(note: SNNote): void { + const references = this.immutableItem.references.filter((ref) => ref.uuid !== note.uuid) + this.mutableContent.references = references + } +} diff --git a/packages/models/src/Domain/Syncable/File/FileProtocolV1.ts b/packages/models/src/Domain/Syncable/File/FileProtocolV1.ts new file mode 100644 index 000000000..815de07c9 --- /dev/null +++ b/packages/models/src/Domain/Syncable/File/FileProtocolV1.ts @@ -0,0 +1,9 @@ +export interface FileProtocolV1 { + readonly encryptionHeader: string + readonly key: string + readonly remoteIdentifier: string +} + +export enum FileProtocolV1Constants { + KeySize = 256, +} diff --git a/packages/models/src/Domain/Syncable/File/index.ts b/packages/models/src/Domain/Syncable/File/index.ts new file mode 100644 index 000000000..180bba421 --- /dev/null +++ b/packages/models/src/Domain/Syncable/File/index.ts @@ -0,0 +1,4 @@ +export * from './File' +export * from './FileMutator' +export * from './FileMetadata' +export * from './FileProtocolV1' diff --git a/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts b/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts new file mode 100644 index 000000000..6271147e9 --- /dev/null +++ b/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts @@ -0,0 +1,19 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem' +import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent' + +export interface ItemsKeyContentSpecialized extends SpecializedContent { + version: ProtocolVersion + isDefault?: boolean | undefined + itemsKey: string + dataAuthenticationKey?: string +} + +export type ItemsKeyContent = ItemsKeyContentSpecialized & ItemContent + +export interface ItemsKeyInterface extends DecryptedItemInterface { + get keyVersion(): ProtocolVersion + get isDefault(): boolean | undefined + get itemsKey(): string + get dataAuthenticationKey(): string | undefined +} diff --git a/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyMutatorInterface.ts b/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyMutatorInterface.ts new file mode 100644 index 000000000..49816c5cd --- /dev/null +++ b/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyMutatorInterface.ts @@ -0,0 +1,5 @@ +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' + +export interface ItemsKeyMutatorInterface extends DecryptedItemMutator { + set isDefault(isDefault: boolean) +} diff --git a/packages/models/src/Domain/Syncable/Note/Note.spec.ts b/packages/models/src/Domain/Syncable/Note/Note.spec.ts new file mode 100644 index 000000000..c5704f1e2 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Note/Note.spec.ts @@ -0,0 +1,42 @@ +import { createNote } from './../../Utilities/Test/SpecUtils' + +describe('SNNote Tests', () => { + it('should safely type required fields of Note when creating from PayloadContent', () => { + const note = createNote({ + title: 'Expected string', + text: ['unexpected array'] as never, + preview_plain: 'Expected preview', + preview_html: {} as never, + hidePreview: 'string' as never, + }) + + expect([ + typeof note.title, + typeof note.text, + typeof note.preview_html, + typeof note.preview_plain, + typeof note.hidePreview, + ]).toStrictEqual(['string', 'string', 'string', 'string', 'boolean']) + }) + + it('should preserve falsy values when casting from PayloadContent', () => { + const note = createNote({ + preview_plain: null as never, + preview_html: undefined, + }) + + expect(note.preview_plain).toBeFalsy() + expect(note.preview_html).toBeFalsy() + }) + + it('should set mobilePrefersPlainEditor when given a valid choice', () => { + const selected = createNote({ + mobilePrefersPlainEditor: true, + }) + + const unselected = createNote() + + expect(selected.mobilePrefersPlainEditor).toBeTruthy() + expect(unselected.mobilePrefersPlainEditor).toBe(undefined) + }) +}) diff --git a/packages/models/src/Domain/Syncable/Note/Note.ts b/packages/models/src/Domain/Syncable/Note/Note.ts new file mode 100644 index 000000000..21a963bea --- /dev/null +++ b/packages/models/src/Domain/Syncable/Note/Note.ts @@ -0,0 +1,34 @@ +import { ContentType } from '@standardnotes/common' +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' +import { AppDataField } from '../../Abstract/Item/Types/AppDataField' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { NoteContent, NoteContentSpecialized } from './NoteContent' + +export const isNote = (x: ItemInterface): x is SNNote => x.content_type === ContentType.Note + +export class SNNote extends DecryptedItem implements NoteContentSpecialized { + public readonly title: string + public readonly text: string + public readonly mobilePrefersPlainEditor?: boolean + public readonly hidePreview: boolean = false + public readonly preview_plain: string + public readonly preview_html: string + public readonly prefersPlainEditor: boolean + public readonly spellcheck?: boolean + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + + this.title = String(this.payload.content.title || '') + this.text = String(this.payload.content.text || '') + this.preview_plain = String(this.payload.content.preview_plain || '') + this.preview_html = String(this.payload.content.preview_html || '') + this.hidePreview = Boolean(this.payload.content.hidePreview) + this.spellcheck = this.payload.content.spellcheck + + this.prefersPlainEditor = this.getAppDomainValueWithDefault(AppDataField.PrefersPlainEditor, false) + + this.mobilePrefersPlainEditor = this.payload.content.mobilePrefersPlainEditor + } +} diff --git a/packages/models/src/Domain/Syncable/Note/NoteContent.ts b/packages/models/src/Domain/Syncable/Note/NoteContent.ts new file mode 100644 index 000000000..6e4202c55 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Note/NoteContent.ts @@ -0,0 +1,13 @@ +import { ItemContent } from '../../Abstract/Content/ItemContent' + +export interface NoteContentSpecialized { + title: string + text: string + mobilePrefersPlainEditor?: boolean + hidePreview?: boolean + preview_plain?: string + preview_html?: string + spellcheck?: boolean +} + +export type NoteContent = NoteContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Syncable/Note/NoteMutator.ts b/packages/models/src/Domain/Syncable/Note/NoteMutator.ts new file mode 100644 index 000000000..5337773e4 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Note/NoteMutator.ts @@ -0,0 +1,41 @@ +import { AppDataField } from '../../Abstract/Item/Types/AppDataField' +import { NoteContent } from './NoteContent' +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' + +export class NoteMutator extends DecryptedItemMutator { + set title(title: string) { + this.mutableContent.title = title + } + + set text(text: string) { + this.mutableContent.text = text + } + + set hidePreview(hidePreview: boolean) { + this.mutableContent.hidePreview = hidePreview + } + + set preview_plain(preview_plain: string) { + this.mutableContent.preview_plain = preview_plain + } + + set preview_html(preview_html: string | undefined) { + this.mutableContent.preview_html = preview_html + } + + set prefersPlainEditor(prefersPlainEditor: boolean) { + this.setAppDataItem(AppDataField.PrefersPlainEditor, prefersPlainEditor) + } + + set spellcheck(spellcheck: boolean) { + this.mutableContent.spellcheck = spellcheck + } + + toggleSpellcheck(): void { + if (this.mutableContent.spellcheck == undefined) { + this.mutableContent.spellcheck = false + } else { + this.mutableContent.spellcheck = !this.mutableContent.spellcheck + } + } +} diff --git a/packages/models/src/Domain/Syncable/Note/index.ts b/packages/models/src/Domain/Syncable/Note/index.ts new file mode 100644 index 000000000..fd1a49467 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Note/index.ts @@ -0,0 +1,3 @@ +export * from './Note' +export * from './NoteMutator' +export * from './NoteContent' diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartView.ts b/packages/models/src/Domain/Syncable/SmartView/SmartView.ts new file mode 100644 index 000000000..21af071a6 --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/SmartView.ts @@ -0,0 +1,44 @@ +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { PredicateInterface, PredicateJsonForm } from '../../Runtime/Predicate/Interface' +import { predicateFromJson } from '../../Runtime/Predicate/Generators' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' + +export const SMART_TAG_DSL_PREFIX = '![' + +export enum SystemViewId { + AllNotes = 'all-notes', + Files = 'files', + ArchivedNotes = 'archived-notes', + TrashedNotes = 'trashed-notes', + UntaggedNotes = 'untagged-notes', +} + +export interface SmartViewContent extends ItemContent { + title: string + predicate: PredicateJsonForm +} + +export function isSystemView(view: SmartView): boolean { + return Object.values(SystemViewId).includes(view.uuid as SystemViewId) +} + +/** + * A tag that defines a predicate that consumers can use + * to retrieve a dynamic list of items. + */ +export class SmartView extends DecryptedItem { + public readonly predicate!: PredicateInterface + public readonly title: string + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + this.title = String(this.content.title || '') + + try { + this.predicate = this.content.predicate && predicateFromJson(this.content.predicate) + } catch (error) { + console.error(error) + } + } +} diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts b/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts new file mode 100644 index 000000000..aa3b00af6 --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts @@ -0,0 +1,179 @@ +import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload' +import { SNNote } from '../Note/Note' +import { SmartViewContent, SmartView, SystemViewId } from './SmartView' +import { ItemWithTags } from '../../Runtime/Display/Search/ItemWithTags' +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { Predicate } from '../../Runtime/Predicate/Predicate' +import { CompoundPredicate } from '../../Runtime/Predicate/CompoundPredicate' +import { PayloadTimestampDefaults } from '../../Abstract/Payload' +import { FilterDisplayOptions } from '../../Runtime/Display' +import { FileItem } from '../File' + +export function BuildSmartViews( + options: FilterDisplayOptions, + { supportsFileNavigation = false }: { supportsFileNavigation: boolean }, +): SmartView[] { + const notes = new SmartView( + new DecryptedPayload({ + uuid: SystemViewId.AllNotes, + content_type: ContentType.SmartView, + ...PayloadTimestampDefaults(), + content: FillItemContent({ + title: 'Notes', + predicate: allNotesPredicate(options).toJson(), + }), + }), + ) + + const files = new SmartView( + new DecryptedPayload({ + uuid: SystemViewId.Files, + content_type: ContentType.SmartView, + ...PayloadTimestampDefaults(), + content: FillItemContent({ + title: 'Files', + predicate: filesPredicate(options).toJson(), + }), + }), + ) + + const archived = new SmartView( + new DecryptedPayload({ + uuid: SystemViewId.ArchivedNotes, + content_type: ContentType.SmartView, + ...PayloadTimestampDefaults(), + content: FillItemContent({ + title: 'Archived', + predicate: archivedNotesPredicate(options).toJson(), + }), + }), + ) + + const trash = new SmartView( + new DecryptedPayload({ + uuid: SystemViewId.TrashedNotes, + content_type: ContentType.SmartView, + ...PayloadTimestampDefaults(), + content: FillItemContent({ + title: 'Trash', + predicate: trashedNotesPredicate(options).toJson(), + }), + }), + ) + + const untagged = new SmartView( + new DecryptedPayload({ + uuid: SystemViewId.UntaggedNotes, + content_type: ContentType.SmartView, + ...PayloadTimestampDefaults(), + content: FillItemContent({ + title: 'Untagged', + predicate: untaggedNotesPredicate(options).toJson(), + }), + }), + ) + + if (supportsFileNavigation) { + return [notes, files, archived, trash, untagged] + } else { + return [notes, archived, trash, untagged] + } +} + +function allNotesPredicate(options: FilterDisplayOptions) { + const subPredicates: Predicate[] = [new Predicate('content_type', '=', ContentType.Note)] + + if (options.includeTrashed === false) { + subPredicates.push(new Predicate('trashed', '=', false)) + } + if (options.includeArchived === false) { + subPredicates.push(new Predicate('archived', '=', false)) + } + if (options.includeProtected === false) { + subPredicates.push(new Predicate('protected', '=', false)) + } + if (options.includePinned === false) { + subPredicates.push(new Predicate('pinned', '=', false)) + } + const predicate = new CompoundPredicate('and', subPredicates) + + return predicate +} + +function filesPredicate(options: FilterDisplayOptions) { + const subPredicates: Predicate[] = [new Predicate('content_type', '=', ContentType.File)] + + if (options.includeTrashed === false) { + subPredicates.push(new Predicate('trashed', '=', false)) + } + if (options.includeArchived === false) { + subPredicates.push(new Predicate('archived', '=', false)) + } + if (options.includeProtected === false) { + subPredicates.push(new Predicate('protected', '=', false)) + } + if (options.includePinned === false) { + subPredicates.push(new Predicate('pinned', '=', false)) + } + const predicate = new CompoundPredicate('and', subPredicates) + + return predicate +} + +function archivedNotesPredicate(options: FilterDisplayOptions) { + const subPredicates: Predicate[] = [ + new Predicate('archived', '=', true), + new Predicate('content_type', '=', ContentType.Note), + ] + if (options.includeTrashed === false) { + subPredicates.push(new Predicate('trashed', '=', false)) + } + if (options.includeProtected === false) { + subPredicates.push(new Predicate('protected', '=', false)) + } + if (options.includePinned === false) { + subPredicates.push(new Predicate('pinned', '=', false)) + } + const predicate = new CompoundPredicate('and', subPredicates) + + return predicate +} + +function trashedNotesPredicate(options: FilterDisplayOptions) { + const subPredicates: Predicate[] = [ + new Predicate('trashed', '=', true), + new Predicate('content_type', '=', ContentType.Note), + ] + if (options.includeArchived === false) { + subPredicates.push(new Predicate('archived', '=', false)) + } + if (options.includeProtected === false) { + subPredicates.push(new Predicate('protected', '=', false)) + } + if (options.includePinned === false) { + subPredicates.push(new Predicate('pinned', '=', false)) + } + const predicate = new CompoundPredicate('and', subPredicates) + + return predicate +} + +function untaggedNotesPredicate(options: FilterDisplayOptions) { + const subPredicates = [ + new Predicate('content_type', '=', ContentType.Note), + new Predicate('tagsCount', '=', 0), + ] + if (options.includeArchived === false) { + subPredicates.push(new Predicate('archived', '=', false)) + } + if (options.includeProtected === false) { + subPredicates.push(new Predicate('protected', '=', false)) + } + if (options.includePinned === false) { + subPredicates.push(new Predicate('pinned', '=', false)) + } + const predicate = new CompoundPredicate('and', subPredicates) + + return predicate +} diff --git a/packages/models/src/Domain/Syncable/SmartView/index.ts b/packages/models/src/Domain/Syncable/SmartView/index.ts new file mode 100644 index 000000000..8692368bf --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/index.ts @@ -0,0 +1,2 @@ +export * from './SmartView' +export * from './SmartViewBuilder' diff --git a/packages/models/src/Domain/Syncable/Tag/Tag.spec.ts b/packages/models/src/Domain/Syncable/Tag/Tag.spec.ts new file mode 100644 index 000000000..ed5ab63b8 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Tag/Tag.spec.ts @@ -0,0 +1,40 @@ +import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource' +import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload' +import { SNTag, TagContent } from './Tag' +import { ContentType } from '@standardnotes/common' +import { FillItemContent } from '../../Abstract/Content/ItemContent' +import { ContentReference } from '../../Abstract/Reference/ContentReference' +import { PayloadTimestampDefaults } from '../../Abstract/Payload' + +const randUuid = () => String(Math.random()) + +const create = (title: string, references: ContentReference[] = []): SNTag => { + const tag = new SNTag( + new DecryptedPayload( + { + uuid: randUuid(), + content_type: ContentType.Tag, + content: FillItemContent({ + title, + references, + } as TagContent), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) + + return tag +} + +describe('SNTag Tests', () => { + it('should count notes in the basic case', () => { + const tag = create('helloworld', [ + { uuid: randUuid(), content_type: ContentType.Note }, + { uuid: randUuid(), content_type: ContentType.Note }, + { uuid: randUuid(), content_type: ContentType.Tag }, + ]) + + expect(tag.noteCount).toEqual(2) + }) +}) diff --git a/packages/models/src/Domain/Syncable/Tag/Tag.ts b/packages/models/src/Domain/Syncable/Tag/Tag.ts new file mode 100644 index 000000000..062c0786e --- /dev/null +++ b/packages/models/src/Domain/Syncable/Tag/Tag.ts @@ -0,0 +1,56 @@ +import { ContentType, Uuid } from '@standardnotes/common' +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { ContentReference } from '../../Abstract/Reference/ContentReference' +import { isTagToParentTagReference } from '../../Abstract/Reference/Functions' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' + +export const TagFolderDelimitter = '.' + +interface TagInterface { + title: string + expanded: boolean +} + +export type TagContent = TagInterface & ItemContent + +export const isTag = (x: ItemInterface): x is SNTag => x.content_type === ContentType.Tag + +export class SNTag extends DecryptedItem implements TagInterface { + public readonly title: string + + /** Whether to render child tags in view hierarchy. Opposite of collapsed. */ + public readonly expanded: boolean + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + this.title = this.payload.content.title || '' + this.expanded = this.payload.content.expanded != undefined ? this.payload.content.expanded : true + } + + get noteReferences(): ContentReference[] { + const references = this.payload.references + return references.filter((ref) => ref.content_type === ContentType.Note) + } + + get noteCount(): number { + return this.noteReferences.length + } + + public get parentId(): Uuid | undefined { + const reference = this.references.find(isTagToParentTagReference) + return reference?.uuid + } + + public static arrayToDisplayString(tags: SNTag[]): string { + return tags + .sort((a, b) => { + return a.title > b.title ? 1 : -1 + }) + .map((tag) => { + return '#' + tag.title + }) + .join(' ') + } +} diff --git a/packages/models/src/Domain/Syncable/Tag/TagMutator.spec.ts b/packages/models/src/Domain/Syncable/Tag/TagMutator.spec.ts new file mode 100644 index 000000000..5141f8644 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Tag/TagMutator.spec.ts @@ -0,0 +1,38 @@ +import { ContentType } from '@standardnotes/common' +import { ContenteReferenceType, MutationType } from '../../Abstract/Item' +import { createFile, createTag } from '../../Utilities/Test/SpecUtils' +import { SNTag } from './Tag' +import { TagMutator } from './TagMutator' + +describe('tag mutator', () => { + it('should add file to tag', () => { + const file = createFile() + + const tag = createTag() + const mutator = new TagMutator(tag, MutationType.UpdateUserTimestamps) + mutator.addFile(file) + const result = mutator.getResult() + + expect(result.content.references[0]).toEqual({ + uuid: file.uuid, + content_type: ContentType.File, + reference_type: ContenteReferenceType.TagToFile, + }) + }) + + it('should remove file from tag', () => { + const file = createFile() + + const tag = createTag() + const addMutator = new TagMutator(tag, MutationType.UpdateUserTimestamps) + addMutator.addFile(file) + const addResult = addMutator.getResult() + + const mutatedTag = new SNTag(addResult) + const removeMutator = new TagMutator(mutatedTag, MutationType.UpdateUserTimestamps) + removeMutator.removeFile(file) + const removeResult = removeMutator.getResult() + + expect(removeResult.content.references).toHaveLength(0) + }) +}) diff --git a/packages/models/src/Domain/Syncable/Tag/TagMutator.ts b/packages/models/src/Domain/Syncable/Tag/TagMutator.ts new file mode 100644 index 000000000..91527163d --- /dev/null +++ b/packages/models/src/Domain/Syncable/Tag/TagMutator.ts @@ -0,0 +1,70 @@ +import { ContentType } from '@standardnotes/common' +import { TagContent, SNTag } from './Tag' +import { FileItem } from '../File' +import { SNNote } from '../Note' +import { isTagToParentTagReference } from '../../Abstract/Reference/Functions' +import { TagToParentTagReference } from '../../Abstract/Reference/TagToParentTagReference' +import { ContenteReferenceType } from '../../Abstract/Reference/ContenteReferenceType' +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' +import { TagToFileReference } from '../../Abstract/Reference/TagToFileReference' + +export class TagMutator extends DecryptedItemMutator { + set title(title: string) { + this.mutableContent.title = title + } + + set expanded(expanded: boolean) { + this.mutableContent.expanded = expanded + } + + public makeChildOf(tag: SNTag): void { + const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref)) + + const reference: TagToParentTagReference = { + reference_type: ContenteReferenceType.TagToParentTag, + content_type: ContentType.Tag, + uuid: tag.uuid, + } + + references.push(reference) + + this.mutableContent.references = references + } + + public unsetParent(): void { + this.mutableContent.references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref)) + } + + public addFile(file: FileItem): void { + if (this.immutableItem.isReferencingItem(file)) { + return + } + + const reference: TagToFileReference = { + reference_type: ContenteReferenceType.TagToFile, + content_type: ContentType.File, + uuid: file.uuid, + } + + this.mutableContent.references.push(reference) + } + + public removeFile(file: FileItem): void { + this.mutableContent.references = this.mutableContent.references.filter((r) => r.uuid !== file.uuid) + } + + public addNote(note: SNNote): void { + if (this.immutableItem.isReferencingItem(note)) { + return + } + + this.mutableContent.references.push({ + uuid: note.uuid, + content_type: note.content_type, + }) + } + + public removeNote(note: SNNote): void { + this.mutableContent.references = this.mutableContent.references.filter((r) => r.uuid !== note.uuid) + } +} diff --git a/packages/models/src/Domain/Syncable/Tag/index.ts b/packages/models/src/Domain/Syncable/Tag/index.ts new file mode 100644 index 000000000..339182ba6 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Tag/index.ts @@ -0,0 +1,2 @@ +export * from './Tag' +export * from './TagMutator' diff --git a/packages/models/src/Domain/Syncable/Theme/Theme.ts b/packages/models/src/Domain/Syncable/Theme/Theme.ts new file mode 100644 index 000000000..b288d4440 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Theme/Theme.ts @@ -0,0 +1,48 @@ +import { ComponentArea } from '@standardnotes/features' +import { SNComponent } from '../Component/Component' +import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy' +import { AppDataField } from '../../Abstract/Item/Types/AppDataField' +import { HistoryEntryInterface } from '../../Runtime/History' +import { DecryptedItemInterface, ItemInterface } from '../../Abstract/Item' +import { ContentType } from '@standardnotes/common' +import { useBoolean } from '@standardnotes/utils' + +export const isTheme = (x: ItemInterface): x is SNTheme => x.content_type === ContentType.Theme + +export class SNTheme extends SNComponent { + public override area: ComponentArea = ComponentArea.Themes + + isLayerable(): boolean { + return useBoolean(this.package_info && this.package_info.layerable, false) + } + + /** Do not duplicate under most circumstances. Always keep original */ + override strategyWhenConflictingWithItem( + _item: DecryptedItemInterface, + _previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + return ConflictStrategy.KeepBase + } + + getMobileRules() { + return ( + this.getAppDomainValue(AppDataField.MobileRules) || { + constants: {}, + rules: {}, + } + ) + } + + /** Same as getMobileRules but without default value. */ + hasMobileRules() { + return this.getAppDomainValue(AppDataField.MobileRules) + } + + getNotAvailOnMobile() { + return this.getAppDomainValue(AppDataField.NotAvailableOnMobile) + } + + isMobileActive() { + return this.getAppDomainValue(AppDataField.MobileActive) + } +} diff --git a/packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts b/packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts new file mode 100644 index 000000000..d102c8f11 --- /dev/null +++ b/packages/models/src/Domain/Syncable/Theme/ThemeMutator.ts @@ -0,0 +1,25 @@ +import { AppDataField } from '../../Abstract/Item/Types/AppDataField' +import { ComponentContent } from '../Component/ComponentContent' +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' + +export class ThemeMutator extends DecryptedItemMutator { + setMobileRules(rules: unknown) { + this.setAppDataItem(AppDataField.MobileRules, rules) + } + + setNotAvailOnMobile(notAvailable: boolean) { + this.setAppDataItem(AppDataField.NotAvailableOnMobile, notAvailable) + } + + set local_url(local_url: string) { + this.mutableContent.local_url = local_url + } + + /** + * We must not use .active because if you set that to true, it will also + * activate that theme on desktop/web + */ + setMobileActive(active: boolean) { + this.setAppDataItem(AppDataField.MobileActive, active) + } +} diff --git a/packages/models/src/Domain/Syncable/Theme/index.ts b/packages/models/src/Domain/Syncable/Theme/index.ts new file mode 100644 index 000000000..97b0c8f3e --- /dev/null +++ b/packages/models/src/Domain/Syncable/Theme/index.ts @@ -0,0 +1,2 @@ +export * from './Theme' +export * from './ThemeMutator' diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts new file mode 100644 index 000000000..c52a1f29e --- /dev/null +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts @@ -0,0 +1,68 @@ +import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort' +import { FeatureIdentifier } from '@standardnotes/features' + +export enum PrefKey { + TagsPanelWidth = 'tagsPanelWidth', + NotesPanelWidth = 'notesPanelWidth', + EditorWidth = 'editorWidth', + EditorLeft = 'editorLeft', + EditorMonospaceEnabled = 'monospaceFont', + EditorSpellcheck = 'spellcheck', + EditorResizersEnabled = 'marginResizersEnabled', + SortNotesBy = 'sortBy', + SortNotesReverse = 'sortReverse', + NotesShowArchived = 'showArchived', + NotesShowTrashed = 'showTrashed', + NotesHideProtected = 'hideProtected', + NotesHidePinned = 'hidePinned', + NotesHideNotePreview = 'hideNotePreview', + NotesHideDate = 'hideDate', + NotesHideTags = 'hideTags', + NotesHideEditorIcon = 'hideEditorIcon', + UseSystemColorScheme = 'useSystemColorScheme', + AutoLightThemeIdentifier = 'autoLightThemeIdentifier', + AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier', + NoteAddToParentFolders = 'noteAddToParentFolders', + MobileSortNotesBy = 'mobileSortBy', + MobileSortNotesReverse = 'mobileSortReverse', + MobileNotesHideNotePreview = 'mobileHideNotePreview', + MobileNotesHideDate = 'mobileHideDate', + MobileNotesHideTags = 'mobileHideTags', + MobileLastExportDate = 'mobileLastExportDate', + MobileDoNotShowAgainUnsupportedEditors = 'mobileDoNotShowAgainUnsupportedEditors', + MobileSelectedTagUuid = 'mobileSelectedTagUuid', + MobileNotesHideEditorIcon = 'mobileHideEditorIcon', +} + +export type PrefValue = { + [PrefKey.TagsPanelWidth]: number + [PrefKey.NotesPanelWidth]: number + [PrefKey.EditorWidth]: number | null + [PrefKey.EditorLeft]: number | null + [PrefKey.EditorMonospaceEnabled]: boolean + [PrefKey.EditorSpellcheck]: boolean + [PrefKey.EditorResizersEnabled]: boolean + [PrefKey.SortNotesBy]: CollectionSortProperty + [PrefKey.SortNotesReverse]: boolean + [PrefKey.NotesShowArchived]: boolean + [PrefKey.NotesShowTrashed]: boolean + [PrefKey.NotesHidePinned]: boolean + [PrefKey.NotesHideProtected]: boolean + [PrefKey.NotesHideNotePreview]: boolean + [PrefKey.NotesHideDate]: boolean + [PrefKey.NotesHideTags]: boolean + [PrefKey.NotesHideEditorIcon]: boolean + [PrefKey.UseSystemColorScheme]: boolean + [PrefKey.AutoLightThemeIdentifier]: FeatureIdentifier | 'Default' + [PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier | 'Default' + [PrefKey.NoteAddToParentFolders]: boolean + [PrefKey.MobileSortNotesBy]: CollectionSortProperty + [PrefKey.MobileSortNotesReverse]: boolean + [PrefKey.MobileNotesHideNotePreview]: boolean + [PrefKey.MobileNotesHideDate]: boolean + [PrefKey.MobileNotesHideTags]: boolean + [PrefKey.MobileLastExportDate]: Date | undefined + [PrefKey.MobileDoNotShowAgainUnsupportedEditors]: boolean + [PrefKey.MobileSelectedTagUuid]: string | undefined + [PrefKey.MobileNotesHideEditorIcon]: boolean +} diff --git a/packages/models/src/Domain/Syncable/UserPrefs/UserPrefs.ts b/packages/models/src/Domain/Syncable/UserPrefs/UserPrefs.ts new file mode 100644 index 000000000..75aeceff2 --- /dev/null +++ b/packages/models/src/Domain/Syncable/UserPrefs/UserPrefs.ts @@ -0,0 +1,20 @@ +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { ContentType } from '@standardnotes/common' +import { Predicate } from '../../Runtime/Predicate/Predicate' +import { PrefKey, PrefValue } from './PrefKey' + +export class SNUserPrefs extends DecryptedItem { + static singletonPredicate = new Predicate('content_type', '=', ContentType.UserPrefs) + + override get isSingleton(): true { + return true + } + + override singletonPredicate(): Predicate { + return SNUserPrefs.singletonPredicate + } + + getPref(key: K): PrefValue[K] | undefined { + return this.getAppDomainValue(key) + } +} diff --git a/packages/models/src/Domain/Syncable/UserPrefs/UserPrefsMutator.ts b/packages/models/src/Domain/Syncable/UserPrefs/UserPrefsMutator.ts new file mode 100644 index 000000000..d8ad7c5f8 --- /dev/null +++ b/packages/models/src/Domain/Syncable/UserPrefs/UserPrefsMutator.ts @@ -0,0 +1,8 @@ +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' +import { PrefKey, PrefValue } from './PrefKey' + +export class UserPrefsMutator extends DecryptedItemMutator { + setPref(key: K, value: PrefValue[K]): void { + this.setAppDataItem(key, value) + } +} diff --git a/packages/models/src/Domain/Syncable/UserPrefs/index.ts b/packages/models/src/Domain/Syncable/UserPrefs/index.ts new file mode 100644 index 000000000..3953a3ae1 --- /dev/null +++ b/packages/models/src/Domain/Syncable/UserPrefs/index.ts @@ -0,0 +1,3 @@ +export * from './UserPrefs' +export * from './UserPrefsMutator' +export * from './PrefKey' diff --git a/packages/models/src/Domain/Utilities/Item/FindItem.ts b/packages/models/src/Domain/Utilities/Item/FindItem.ts new file mode 100644 index 000000000..13092e2ca --- /dev/null +++ b/packages/models/src/Domain/Utilities/Item/FindItem.ts @@ -0,0 +1,10 @@ +import { Uuid } from '@standardnotes/common' +import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' + +export function FindItem(items: I[], uuid: Uuid): I | undefined { + return items.find((item) => item.uuid === uuid) +} + +export function SureFindItem(items: I[], uuid: Uuid): I { + return FindItem(items, uuid) as I +} diff --git a/packages/models/src/Domain/Utilities/Item/ItemContentsDiffer.ts b/packages/models/src/Domain/Utilities/Item/ItemContentsDiffer.ts new file mode 100644 index 000000000..c99631c43 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Item/ItemContentsDiffer.ts @@ -0,0 +1,16 @@ +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { ItemContentsEqual } from './ItemContentsEqual' + +export function ItemContentsDiffer( + item1: DecryptedItemInterface, + item2: DecryptedItemInterface, + excludeContentKeys: (keyof ItemContent)[] = [], +) { + return !ItemContentsEqual( + item1.content as ItemContent, + item2.content as ItemContent, + [...item1.contentKeysToIgnoreWhenCheckingEquality(), ...excludeContentKeys], + item1.appDataContentKeysToIgnoreWhenCheckingEquality(), + ) +} diff --git a/packages/models/src/Domain/Utilities/Item/ItemContentsEqual.ts b/packages/models/src/Domain/Utilities/Item/ItemContentsEqual.ts new file mode 100644 index 000000000..e138b8ac3 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Item/ItemContentsEqual.ts @@ -0,0 +1,47 @@ +import { omitInPlace, sortedCopy } from '@standardnotes/utils' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DefaultAppDomain } from '../../Abstract/Item/Types/DefaultAppDomain' +import { AppDataField } from '../../Abstract/Item/Types/AppDataField' + +export function ItemContentsEqual( + leftContent: C, + rightContent: C, + keysToIgnore: (keyof C)[], + appDataKeysToIgnore: AppDataField[], +) { + /* Create copies of objects before running omit as not to modify source values directly. */ + const leftContentCopy: Partial = sortedCopy(leftContent) + if (leftContentCopy.appData) { + const domainData = leftContentCopy.appData[DefaultAppDomain] + omitInPlace(domainData, appDataKeysToIgnore) + /** + * We don't want to disqualify comparison if one object contains an empty domain object + * and the other doesn't contain a domain object. This can happen if you create an item + * without setting dirty, which means it won't be initialized with a client_updated_at + */ + if (domainData) { + if (Object.keys(domainData).length === 0) { + delete leftContentCopy.appData + } + } else { + delete leftContentCopy.appData + } + } + omitInPlace>(leftContentCopy, keysToIgnore) + + const rightContentCopy: Partial = sortedCopy(rightContent) + if (rightContentCopy.appData) { + const domainData = rightContentCopy.appData[DefaultAppDomain] + omitInPlace(domainData, appDataKeysToIgnore) + if (domainData) { + if (Object.keys(domainData).length === 0) { + delete rightContentCopy.appData + } + } else { + delete rightContentCopy.appData + } + } + omitInPlace>(rightContentCopy, keysToIgnore) + + return JSON.stringify(leftContentCopy) === JSON.stringify(rightContentCopy) +} diff --git a/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts b/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts new file mode 100644 index 000000000..5e5793c02 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts @@ -0,0 +1,113 @@ +import { ContentType } from '@standardnotes/common' +import { EncryptedItem } from '../../Abstract/Item/Implementations/EncryptedItem' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { FileItem } from '../../Syncable/File/File' +import { SNFeatureRepo } from '../../Syncable/FeatureRepo/FeatureRepo' +import { SNActionsExtension } from '../../Syncable/ActionsExtension/ActionsExtension' +import { SNComponent } from '../../Syncable/Component/Component' +import { SNEditor } from '../../Syncable/Editor/Editor' +import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' +import { SNNote } from '../../Syncable/Note/Note' +import { SmartView } from '../../Syncable/SmartView/SmartView' +import { SNTag } from '../../Syncable/Tag/Tag' +import { SNTheme } from '../../Syncable/Theme/Theme' +import { SNUserPrefs } from '../../Syncable/UserPrefs/UserPrefs' +import { FileMutator } from '../../Syncable/File/FileMutator' +import { MutationType } from '../../Abstract/Item/Types/MutationType' +import { ThemeMutator } from '../../Syncable/Theme/ThemeMutator' +import { UserPrefsMutator } from '../../Syncable/UserPrefs/UserPrefsMutator' +import { ActionsExtensionMutator } from '../../Syncable/ActionsExtension/ActionsExtensionMutator' +import { ComponentMutator } from '../../Syncable/Component/ComponentMutator' +import { TagMutator } from '../../Syncable/Tag/TagMutator' +import { NoteMutator } from '../../Syncable/Note/NoteMutator' +import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' +import { + DeletedPayloadInterface, + EncryptedPayloadInterface, + isDecryptedPayload, + isDeletedPayload, + isEncryptedPayload, +} from '../../Abstract/Payload' +import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem' +import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem' +import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem' + +type ItemClass = new (payload: DecryptedPayloadInterface) => DecryptedItem + +type MutatorClass = new ( + item: DecryptedItemInterface, + type: MutationType, +) => DecryptedItemMutator + +type MappingEntry = { + itemClass: ItemClass + mutatorClass?: MutatorClass +} + +const ContentTypeClassMapping: Partial> = { + [ContentType.ActionsExtension]: { + itemClass: SNActionsExtension, + mutatorClass: ActionsExtensionMutator, + }, + [ContentType.Component]: { itemClass: SNComponent, mutatorClass: ComponentMutator }, + [ContentType.Editor]: { itemClass: SNEditor }, + [ContentType.ExtensionRepo]: { itemClass: SNFeatureRepo }, + [ContentType.File]: { itemClass: FileItem, mutatorClass: FileMutator }, + [ContentType.Note]: { itemClass: SNNote, mutatorClass: NoteMutator }, + [ContentType.SmartView]: { itemClass: SmartView, mutatorClass: TagMutator }, + [ContentType.Tag]: { itemClass: SNTag, mutatorClass: TagMutator }, + [ContentType.Theme]: { itemClass: SNTheme, mutatorClass: ThemeMutator }, + [ContentType.UserPrefs]: { itemClass: SNUserPrefs, mutatorClass: UserPrefsMutator }, +} as unknown as Partial> + +export function CreateDecryptedMutatorForItem< + I extends DecryptedItemInterface, + M extends DecryptedItemMutator = DecryptedItemMutator, +>(item: I, type: MutationType): M { + const lookupValue = ContentTypeClassMapping[item.content_type]?.mutatorClass + if (lookupValue) { + return new lookupValue(item, type) as M + } else { + return new DecryptedItemMutator(item, type) as M + } +} + +export function RegisterItemClass< + C extends ItemContent = ItemContent, + M extends DecryptedItemMutator = DecryptedItemMutator, +>(contentType: ContentType, itemClass: ItemClass, mutatorClass: M) { + const entry: MappingEntry = { + itemClass: itemClass, + mutatorClass: mutatorClass as unknown as MutatorClass, + } + ContentTypeClassMapping[contentType] = entry as unknown as MappingEntry +} + +export function CreateDecryptedItemFromPayload< + C extends ItemContent = ItemContent, + T extends DecryptedItemInterface = DecryptedItemInterface, +>(payload: DecryptedPayloadInterface): T { + const lookupClass = ContentTypeClassMapping[payload.content_type] + const itemClass = lookupClass ? lookupClass.itemClass : DecryptedItem + const item = new itemClass(payload) + return item as unknown as T +} + +export function CreateItemFromPayload< + C extends ItemContent = ItemContent, + T extends DecryptedItemInterface = DecryptedItemInterface, +>( + payload: DecryptedPayloadInterface | EncryptedPayloadInterface | DeletedPayloadInterface, +): EncryptedItemInterface | DeletedItemInterface | T { + if (isDecryptedPayload(payload)) { + return CreateDecryptedItemFromPayload(payload) + } else if (isEncryptedPayload(payload)) { + return new EncryptedItem(payload) + } else if (isDeletedPayload(payload)) { + return new DeletedItem(payload) + } else { + throw Error('Unhandled case in CreateItemFromPayload') + } +} diff --git a/packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts b/packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts new file mode 100644 index 000000000..a75e37c65 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts @@ -0,0 +1,55 @@ +import { DecryptedPayloadInterface } from './../../Abstract/Payload/Interfaces/DecryptedPayload' +import { ComponentContent } from '../../Syncable/Component/ComponentContent' +import { ComponentArea } from '@standardnotes/features' +import { ContentType } from '@standardnotes/common' +import { ComponentMutator, SNComponent } from '../../Syncable/Component' +import { CreateDecryptedItemFromPayload } from '../Item/ItemGenerator' +import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection' +import { MutationType } from '../../Abstract/Item/Types/MutationType' +import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes' +import { isDecryptedPayload } from '../../Abstract/Payload' +import { SyncResolvedPayload } from '../../Runtime/Deltas/Utilities/SyncResolvedPayload' + +export type AffectorFunction = ( + basePayload: FullyFormedPayloadInterface, + duplicatePayload: FullyFormedPayloadInterface, + baseCollection: ImmutablePayloadCollection, +) => SyncResolvedPayload[] + +const NoteDuplicationAffectedPayloads: AffectorFunction = ( + basePayload: FullyFormedPayloadInterface, + duplicatePayload: FullyFormedPayloadInterface, + baseCollection: ImmutablePayloadCollection, +) => { + /** If note has editor, maintain editor relationship in duplicate note */ + const components = baseCollection + .all(ContentType.Component) + .filter(isDecryptedPayload) + .map((payload) => { + return CreateDecryptedItemFromPayload( + payload as DecryptedPayloadInterface, + ) + }) + + const editor = components + .filter((c) => c.area === ComponentArea.Editor) + .find((e) => { + return e.isExplicitlyEnabledForItem(basePayload.uuid) + }) + + if (!editor) { + return [] + } + + /** Modify the editor to include new note */ + const mutator = new ComponentMutator(editor, MutationType.NoUpdateUserTimestamps) + mutator.associateWithItem(duplicatePayload.uuid) + + const result = mutator.getResult() as SyncResolvedPayload + + return [result] +} + +export const AffectorMapping = { + [ContentType.Note]: NoteDuplicationAffectedPayloads, +} as Partial> diff --git a/packages/models/src/Domain/Utilities/Payload/ConditionalPayloadType.ts b/packages/models/src/Domain/Utilities/Payload/ConditionalPayloadType.ts new file mode 100644 index 000000000..caebd9606 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/ConditionalPayloadType.ts @@ -0,0 +1,20 @@ +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { DeletedPayloadInterface } from '../../Abstract/Payload/Interfaces/DeletedPayload' +import { EncryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/EncryptedPayload' +import { + DecryptedTransferPayload, + DeletedTransferPayload, + EncryptedTransferPayload, +} from '../../Abstract/TransferPayload' + +export type ConditionalPayloadType = T extends DecryptedTransferPayload + ? DecryptedPayloadInterface + : T extends EncryptedTransferPayload + ? EncryptedPayloadInterface + : DeletedPayloadInterface + +export type ConditionalTransferPayloadType

= P extends DecryptedPayloadInterface + ? DecryptedTransferPayload + : P extends EncryptedPayloadInterface + ? EncryptedTransferPayload + : DeletedTransferPayload diff --git a/packages/models/src/Domain/Utilities/Payload/CopyPayloadWithContentOverride.ts b/packages/models/src/Domain/Utilities/Payload/CopyPayloadWithContentOverride.ts new file mode 100644 index 000000000..f0533f84e --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/CopyPayloadWithContentOverride.ts @@ -0,0 +1,19 @@ +import { CreatePayload } from './CreatePayload' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedTransferPayload } from '../../Abstract/TransferPayload' + +export function CopyPayloadWithContentOverride( + payload: DecryptedPayloadInterface, + contentOverride: Partial, +): DecryptedPayloadInterface { + const params: DecryptedTransferPayload = { + ...payload.ejected(), + content: { + ...payload.content, + ...contentOverride, + }, + } + const result = CreatePayload(params, payload.source) + return result +} diff --git a/packages/models/src/Domain/Utilities/Payload/CreatePayload.ts b/packages/models/src/Domain/Utilities/Payload/CreatePayload.ts new file mode 100644 index 000000000..98329eb45 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/CreatePayload.ts @@ -0,0 +1,26 @@ +import { EncryptedPayload } from '../../Abstract/Payload/Implementations/EncryptedPayload' +import { DeletedPayload } from '../../Abstract/Payload/Implementations/DeletedPayload' +import { DecryptedPayload } from '../../Abstract/Payload/Implementations/DecryptedPayload' +import { + FullyFormedTransferPayload, + isDecryptedTransferPayload, + isDeletedTransferPayload, + isEncryptedTransferPayload, +} from '../../Abstract/TransferPayload' +import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource' +import { ConditionalPayloadType } from './ConditionalPayloadType' + +export function CreatePayload( + from: T, + source: PayloadSource, +): ConditionalPayloadType { + if (isDecryptedTransferPayload(from)) { + return new DecryptedPayload(from, source) as unknown as ConditionalPayloadType + } else if (isEncryptedTransferPayload(from)) { + return new EncryptedPayload(from, source) as unknown as ConditionalPayloadType + } else if (isDeletedTransferPayload(from)) { + return new DeletedPayload(from, source) as unknown as ConditionalPayloadType + } else { + throw Error('Unhandled case in CreatePayload') + } +} diff --git a/packages/models/src/Domain/Utilities/Payload/FindPayload.ts b/packages/models/src/Domain/Utilities/Payload/FindPayload.ts new file mode 100644 index 000000000..edce5103f --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/FindPayload.ts @@ -0,0 +1,10 @@ +import { Uuid } from '@standardnotes/common' +import { PayloadInterface } from '../../Abstract/Payload/Interfaces/PayloadInterface' + +export function FindPayload

(payloads: P[], uuid: Uuid): P | undefined { + return payloads.find((payload) => payload.uuid === uuid) +} + +export function SureFindPayload

(payloads: P[], uuid: Uuid): P { + return FindPayload(payloads, uuid) as P +} diff --git a/packages/models/src/Domain/Utilities/Payload/PayloadContentsEqual.ts b/packages/models/src/Domain/Utilities/Payload/PayloadContentsEqual.ts new file mode 100644 index 000000000..f0b28f765 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/PayloadContentsEqual.ts @@ -0,0 +1,15 @@ +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { CreateDecryptedItemFromPayload } from '../Item/ItemGenerator' + +/** + * Compares the .content fields for equality, creating new SNItem objects + * to properly handle .content intricacies. + */ +export function PayloadContentsEqual( + payloadA: DecryptedPayloadInterface, + payloadB: DecryptedPayloadInterface, +): boolean { + const itemA = CreateDecryptedItemFromPayload(payloadA) + const itemB = CreateDecryptedItemFromPayload(payloadB) + return itemA.isItemContentEqualWith(itemB) +} diff --git a/packages/models/src/Domain/Utilities/Payload/PayloadSplit.ts b/packages/models/src/Domain/Utilities/Payload/PayloadSplit.ts new file mode 100644 index 000000000..276920af6 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/PayloadSplit.ts @@ -0,0 +1,98 @@ +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { DeletedPayloadInterface } from '../../Abstract/Payload/Interfaces/DeletedPayload' +import { EncryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/EncryptedPayload' +import { isDecryptedPayload, isDeletedPayload, isEncryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' +import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes' + +export interface PayloadSplit { + encrypted: EncryptedPayloadInterface[] + decrypted: DecryptedPayloadInterface[] + deleted: DeletedPayloadInterface[] +} + +export interface PayloadSplitWithDiscardables { + encrypted: EncryptedPayloadInterface[] + decrypted: DecryptedPayloadInterface[] + deleted: DeletedPayloadInterface[] + discardable: DeletedPayloadInterface[] +} + +export interface NonDecryptedPayloadSplit { + encrypted: EncryptedPayloadInterface[] + deleted: DeletedPayloadInterface[] +} + +export function CreatePayloadSplit( + payloads: FullyFormedPayloadInterface[], +): PayloadSplit { + const split: PayloadSplit = { + encrypted: [], + decrypted: [], + deleted: [], + } + + for (const payload of payloads) { + if (isDecryptedPayload(payload)) { + split.decrypted.push(payload) + } else if (isEncryptedPayload(payload)) { + split.encrypted.push(payload) + } else if (isDeletedPayload(payload)) { + split.deleted.push(payload) + } else { + throw Error('Unhandled case in CreatePayloadSplit') + } + } + + return split +} + +export function CreatePayloadSplitWithDiscardables( + payloads: FullyFormedPayloadInterface[], +): PayloadSplitWithDiscardables { + const split: PayloadSplitWithDiscardables = { + encrypted: [], + decrypted: [], + deleted: [], + discardable: [], + } + + for (const payload of payloads) { + if (isDecryptedPayload(payload)) { + split.decrypted.push(payload) + } else if (isEncryptedPayload(payload)) { + split.encrypted.push(payload) + } else if (isDeletedPayload(payload)) { + if (payload.discardable) { + split.discardable.push(payload) + } else { + split.deleted.push(payload) + } + } else { + throw Error('Unhandled case in CreatePayloadSplitWithDiscardables') + } + } + + return split +} + +export function CreateNonDecryptedPayloadSplit( + payloads: (EncryptedPayloadInterface | DeletedPayloadInterface)[], +): NonDecryptedPayloadSplit { + const split: NonDecryptedPayloadSplit = { + encrypted: [], + deleted: [], + } + + for (const payload of payloads) { + if (isEncryptedPayload(payload)) { + split.encrypted.push(payload) + } else if (isDeletedPayload(payload)) { + split.deleted.push(payload) + } else { + throw Error('Unhandled case in CreateNonDecryptedPayloadSplit') + } + } + + return split +} diff --git a/packages/models/src/Domain/Utilities/Payload/PayloadsByAlternatingUuid.ts b/packages/models/src/Domain/Utilities/Payload/PayloadsByAlternatingUuid.ts new file mode 100644 index 000000000..01442ad37 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/PayloadsByAlternatingUuid.ts @@ -0,0 +1,97 @@ +import { DeletedPayload } from '../../Abstract/Payload/Implementations/DeletedPayload' +import { ContentType } from '@standardnotes/common' +import { extendArray, UuidGenerator } from '@standardnotes/utils' +import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { isEncryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' +import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes' +import { EncryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/EncryptedPayload' +import { PayloadsByUpdatingReferencingPayloadReferences } from './PayloadsByUpdatingReferencingPayloadReferences' +import { SyncResolvedPayload } from '../../Runtime/Deltas/Utilities/SyncResolvedPayload' +import { getIncrementedDirtyIndex } from '../../Runtime/DirtyCounter/DirtyCounter' + +/** + * Return the payloads that result if you alternated the uuid for the payload. + * Alternating a UUID involves instructing related items to drop old references of a uuid + * for the new one. + * @returns An array of payloads that have changed as a result of copying. + */ + +export function PayloadsByAlternatingUuid

( + payload: P, + baseCollection: ImmutablePayloadCollection, +): SyncResolvedPayload[] { + const results: SyncResolvedPayload[] = [] + /** + * We need to clone payload and give it a new uuid, + * then delete item with old uuid from db (cannot modify uuids in our IndexedDB setup) + */ + const copy = payload.copyAsSyncResolved({ + uuid: UuidGenerator.GenerateUuid(), + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + lastSyncBegan: undefined, + lastSyncEnd: new Date(), + duplicate_of: payload.uuid, + }) + + results.push(copy) + + /** + * Get the payloads that make reference to payload and remove + * payload as a relationship, instead adding the new copy. + */ + const updatedReferencing = PayloadsByUpdatingReferencingPayloadReferences( + payload, + baseCollection, + [copy], + [payload.uuid], + ) + + extendArray(results, updatedReferencing) + + if (payload.content_type === ContentType.ItemsKey) { + /** + * Update any payloads who are still encrypted and whose items_key_id point to this uuid + */ + const matchingPayloads = baseCollection + .all() + .filter((p) => isEncryptedPayload(p) && p.items_key_id === payload.uuid) as EncryptedPayloadInterface[] + + const adjustedPayloads = matchingPayloads.map((a) => + a.copyAsSyncResolved({ + items_key_id: copy.uuid, + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + lastSyncEnd: new Date(), + }), + ) + + if (adjustedPayloads.length > 0) { + extendArray(results, adjustedPayloads) + } + } + + const deletedSelf = new DeletedPayload( + { + created_at: payload.created_at, + updated_at: payload.updated_at, + created_at_timestamp: payload.created_at_timestamp, + updated_at_timestamp: payload.updated_at_timestamp, + /** + * Do not set as dirty; this item is non-syncable + * and should be immediately discarded + */ + dirty: false, + content: undefined, + uuid: payload.uuid, + content_type: payload.content_type, + deleted: true, + }, + payload.source, + ) + + results.push(deletedSelf as SyncResolvedPayload) + + return results +} diff --git a/packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts b/packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts new file mode 100644 index 000000000..755ff980e --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts @@ -0,0 +1,81 @@ +import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource' +import { extendArray, UuidGenerator } from '@standardnotes/utils' +import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { AffectorMapping } from './AffectorFunction' +import { PayloadsByUpdatingReferencingPayloadReferences } from './PayloadsByUpdatingReferencingPayloadReferences' +import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' +import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes' +import { SyncResolvedPayload } from '../../Runtime/Deltas/Utilities/SyncResolvedPayload' +import { getIncrementedDirtyIndex } from '../../Runtime/DirtyCounter/DirtyCounter' + +/** + * Copies payload and assigns it a new uuid. + * @returns An array of payloads that have changed as a result of copying. + */ +export function PayloadsByDuplicating(dto: { + payload: FullyFormedPayloadInterface + baseCollection: ImmutablePayloadCollection + isConflict?: boolean + additionalContent?: Partial + source?: PayloadSource +}): SyncResolvedPayload[] { + const { payload, baseCollection, isConflict, additionalContent, source } = dto + + const results: SyncResolvedPayload[] = [] + + const override = { + uuid: UuidGenerator.GenerateUuid(), + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + lastSyncBegan: undefined, + lastSyncEnd: new Date(), + duplicate_of: payload.uuid, + } + + let copy: SyncResolvedPayload + + if (isDecryptedPayload(payload)) { + const contentOverride: C = { + ...payload.content, + ...additionalContent, + } + + if (isConflict) { + contentOverride.conflict_of = payload.uuid + } + + copy = payload.copyAsSyncResolved({ + ...override, + content: contentOverride, + deleted: false, + }) + } else { + copy = payload.copyAsSyncResolved( + { + ...override, + }, + source || payload.source, + ) + } + + results.push(copy) + + if (isDecryptedPayload(payload) && isDecryptedPayload(copy)) { + /** + * Get the payloads that make reference to payload and add the copy. + */ + const updatedReferencing = PayloadsByUpdatingReferencingPayloadReferences(payload, baseCollection, [copy]) + extendArray(results, updatedReferencing) + } + + const affector = AffectorMapping[payload.content_type] + if (affector) { + const affected = affector(payload, copy, baseCollection) + if (affected) { + extendArray(results, affected) + } + } + + return results +} diff --git a/packages/models/src/Domain/Utilities/Payload/PayloadsByUpdatingReferencingPayloadReferences.ts b/packages/models/src/Domain/Utilities/Payload/PayloadsByUpdatingReferencingPayloadReferences.ts new file mode 100644 index 000000000..0c06d83d7 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Payload/PayloadsByUpdatingReferencingPayloadReferences.ts @@ -0,0 +1,52 @@ +import { Uuid } from '@standardnotes/common' +import { remove } from 'lodash' +import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection' +import { ContentReference } from '../../Abstract/Reference/ContentReference' +import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' +import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes' +import { isDecryptedPayload } from '../../Abstract/Payload' +import { SyncResolvedPayload } from '../../Runtime/Deltas/Utilities/SyncResolvedPayload' +import { getIncrementedDirtyIndex } from '../../Runtime/DirtyCounter/DirtyCounter' + +export function PayloadsByUpdatingReferencingPayloadReferences( + payload: DecryptedPayloadInterface, + baseCollection: ImmutablePayloadCollection, + add: FullyFormedPayloadInterface[] = [], + removeIds: Uuid[] = [], +): SyncResolvedPayload[] { + const referencingPayloads = baseCollection.elementsReferencingElement(payload).filter(isDecryptedPayload) + + const results: SyncResolvedPayload[] = [] + + for (const referencingPayload of referencingPayloads) { + const references = referencingPayload.content.references.slice() + const reference = referencingPayload.getReference(payload.uuid) + + for (const addPayload of add) { + const newReference: ContentReference = { + ...reference, + uuid: addPayload.uuid, + content_type: addPayload.content_type, + } + references.push(newReference) + } + + for (const id of removeIds) { + remove(references, { uuid: id }) + } + + const result = referencingPayload.copyAsSyncResolved({ + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + lastSyncEnd: new Date(), + content: { + ...referencingPayload.content, + references, + }, + }) + + results.push(result) + } + + return results +} diff --git a/packages/models/src/Domain/Utilities/Test/SpecUtils.ts b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts new file mode 100644 index 000000000..9b7f7de66 --- /dev/null +++ b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts @@ -0,0 +1,80 @@ +import { TagContent } from './../../Syncable/Tag/Tag' +import { ContentType } from '@standardnotes/common' +import { FillItemContent, ItemContent } from '../../Abstract/Content/ItemContent' +import { DecryptedPayload, PayloadSource, PayloadTimestampDefaults } from '../../Abstract/Payload' +import { FileContent, FileItem } from '../../Syncable/File' +import { NoteContent, SNNote } from '../../Syncable/Note' +import { SNTag } from '../../Syncable/Tag' + +let currentId = 0 + +export const mockUuid = () => { + return `${currentId++}` +} + +export const createNote = (payload?: Partial): SNNote => { + return new SNNote( + new DecryptedPayload( + { + uuid: mockUuid(), + content_type: ContentType.Note, + content: FillItemContent({ ...payload }), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) +} + +export const createNoteWithContent = (content: Partial, createdAt?: Date): SNNote => { + return new SNNote( + new DecryptedPayload( + { + uuid: mockUuid(), + content_type: ContentType.Note, + content: FillItemContent(content), + ...PayloadTimestampDefaults(), + created_at: createdAt || new Date(), + }, + PayloadSource.Constructor, + ), + ) +} + +export const createTag = (title = 'photos') => { + return new SNTag( + new DecryptedPayload( + { + uuid: mockUuid(), + content_type: ContentType.Tag, + content: FillItemContent({ title }), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) +} + +export const createFile = (name = 'screenshot.png') => { + return new FileItem( + new DecryptedPayload( + { + uuid: mockUuid(), + content_type: ContentType.File, + content: FillItemContent({ name }), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) +} + +export const pinnedContent = (): Partial => { + return { + appData: { + 'org.standardnotes.sn': { + pinned: true, + }, + }, + } +} diff --git a/packages/models/src/Domain/index.ts b/packages/models/src/Domain/index.ts new file mode 100644 index 000000000..a11b7ce49 --- /dev/null +++ b/packages/models/src/Domain/index.ts @@ -0,0 +1,55 @@ +export * from './Abstract/Content/ItemContent' +export * from './Abstract/Contextual' +export * from './Abstract/Item' +export * from './Abstract/Payload' +export * from './Abstract/TransferPayload' +export * from './Local/KeyParams/RootKeyParamsInterface' +export * from './Local/RootKey/KeychainTypes' +export * from './Local/RootKey/RootKeyContent' +export * from './Local/RootKey/RootKeyInterface' +export * from './Runtime/Collection/CollectionSort' +export * from './Runtime/Collection/Item/ItemCollection' +export * from './Runtime/Collection/Item/TagNotesIndex' +export * from './Runtime/Collection/Payload/ImmutablePayloadCollection' +export * from './Runtime/Collection/Payload/PayloadCollection' +export * from './Runtime/Deltas' +export * from './Runtime/DirtyCounter/DirtyCounter' +export * from './Runtime/Display/ItemDisplayController' +export * from './Runtime/Display/Types' +export * from './Runtime/Display' +export * from './Runtime/History' +export * from './Runtime/Index/ItemDelta' +export * from './Runtime/Index/SNIndex' +export * from './Runtime/Predicate/CompoundPredicate' +export * from './Runtime/Predicate/Generators' +export * from './Runtime/Predicate/IncludesPredicate' +export * from './Runtime/Predicate/Interface' +export * from './Runtime/Predicate/Interface' +export * from './Runtime/Predicate/NotPredicate' +export * from './Runtime/Predicate/Operator' +export * from './Runtime/Predicate/Predicate' +export * from './Runtime/Predicate/Utils' +export * from './Syncable/ActionsExtension' +export * from './Syncable/Component' +export * from './Syncable/Editor' +export * from './Syncable/FeatureRepo' +export * from './Syncable/File' +export * from './Syncable/ItemsKey/ItemsKeyInterface' +export * from './Syncable/ItemsKey/ItemsKeyMutatorInterface' +export * from './Syncable/Note' +export * from './Syncable/SmartView' +export * from './Syncable/Tag' +export * from './Syncable/Theme' +export * from './Syncable/UserPrefs' +export * from './Utilities/Item/FindItem' +export * from './Utilities/Item/ItemContentsDiffer' +export * from './Utilities/Item/ItemContentsEqual' +export * from './Utilities/Item/ItemGenerator' +export * from './Utilities/Payload/AffectorFunction' +export * from './Utilities/Payload/CopyPayloadWithContentOverride' +export * from './Utilities/Payload/CreatePayload' +export * from './Utilities/Payload/FindPayload' +export * from './Utilities/Payload/PayloadContentsEqual' +export * from './Utilities/Payload/PayloadsByAlternatingUuid' +export * from './Utilities/Payload/PayloadsByDuplicating' +export * from './Utilities/Payload/PayloadSplit' diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts new file mode 100644 index 000000000..920deacdb --- /dev/null +++ b/packages/models/src/index.ts @@ -0,0 +1 @@ +export * from './Domain' diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json new file mode 100644 index 000000000..f3dac14ef --- /dev/null +++ b/packages/models/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../node_modules/@standardnotes/config/src/tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist", + }, + "include": [ + "src/**/*" + ], + "references": [], + "exclude": ["**/*.spec.ts", "dist", "node_modules"] +} diff --git a/yarn.lock b/yarn.lock index f08e58925..76ed598bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6547,7 +6547,7 @@ __metadata: dependencies: "@standardnotes/common": ^1.23.1 "@standardnotes/config": 2.4.3 - "@standardnotes/models": ^1.11.13 + "@standardnotes/models": "workspace:*" "@standardnotes/responses": ^1.6.39 "@standardnotes/services": ^1.13.23 "@standardnotes/sncrypto-common": ^1.9.0 @@ -6624,7 +6624,7 @@ __metadata: "@standardnotes/common": ^1.23.1 "@standardnotes/encryption": "workspace:*" "@standardnotes/filepicker": "workspace:*" - "@standardnotes/models": ^1.11.13 + "@standardnotes/models": "workspace:*" "@standardnotes/responses": ^1.6.39 "@standardnotes/services": ^1.13.23 "@standardnotes/sncrypto-common": ^1.9.0 @@ -7006,27 +7006,24 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/models@npm:^1.11.12": - version: 1.11.12 - resolution: "@standardnotes/models@npm:1.11.12" +"@standardnotes/models@^1.11.12, @standardnotes/models@^1.11.13, @standardnotes/models@workspace:*, @standardnotes/models@workspace:packages/models": + version: 0.0.0-use.local + resolution: "@standardnotes/models@workspace:packages/models" dependencies: - "@standardnotes/features": ^1.46.0 - "@standardnotes/responses": ^1.6.38 - "@standardnotes/utils": ^1.6.12 - checksum: 636897db975e59989da86bcf2a6f32a4f8e24c45e41f9f9ee7c6960e47f3d04bcc9b74aadb9c4b488d98b61aca284699ed8b2a857d588493300358c62d69e580 - languageName: node - linkType: hard - -"@standardnotes/models@npm:^1.11.13": - version: 1.11.13 - resolution: "@standardnotes/models@npm:1.11.13" - dependencies: - "@standardnotes/features": ^1.47.0 + "@standardnotes/common": ^1.23.1 + "@standardnotes/features": "workspace:*" "@standardnotes/responses": ^1.6.39 "@standardnotes/utils": ^1.6.12 - checksum: 063f4382b8559f23a81db0a3aab7eca023c926f2069182f364da6dbfb0d312c300bc9dd29a1d389ba5fa1e7c04d8375c625bfc4eb073cad938511493e2976b5a - languageName: node - linkType: hard + "@types/jest": ^27.4.1 + "@types/lodash": ^4.14.182 + "@typescript-eslint/eslint-plugin": ^5.30.0 + eslint-plugin-prettier: ^4.2.1 + jest: ^27.5.1 + lodash: ^4.17.21 + reflect-metadata: ^0.1.13 + ts-jest: ^27.1.3 + languageName: unknown + linkType: soft "@standardnotes/react-native-aes@npm:^1.4.3": version: 1.4.3