internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -1,14 +1,13 @@
import { ContentType } from '@standardnotes/common'
import { assert, naturalSort, removeFromArray, UuidGenerator, Uuids } from '@standardnotes/utils'
import { ItemsKeyMutator, SNItemsKey } from '@standardnotes/encryption'
import { SNItemsKey } from '@standardnotes/encryption'
import { PayloadManager } from '../Payloads/PayloadManager'
import { TagsToFoldersMigrationApplicator } from '../../Migrations/Applicators/TagsToFolders'
import { UuidString } from '../../Types/UuidString'
import * as Models from '@standardnotes/models'
import * as Services from '@standardnotes/services'
import { PayloadManagerChangeData } from '../Payloads'
import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services'
import { CollectionSort, DecryptedItemInterface, ItemContent, SmartViewDefaultIconName } from '@standardnotes/models'
import { ItemRelationshipDirection } from '@standardnotes/services'
type ItemsChangeObserver<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface> = {
contentType: ContentType[]
@@ -23,18 +22,18 @@ type ItemsChangeObserver<I extends Models.DecryptedItemInterface = Models.Decryp
* will then notify its observers (which is us), we'll convert the payloads to items,
* and then we'll propagate them to our listeners.
*/
export class ItemManager
extends Services.AbstractService
implements Services.ItemManagerInterface, ItemsClientInterface
{
export class ItemManager extends Services.AbstractService implements Services.ItemManagerInterface {
private unsubChangeObserver: () => void
private observers: ItemsChangeObserver[] = []
private collection!: Models.ItemCollection
private systemSmartViews: Models.SmartView[]
private tagItemsIndex!: Models.TagItemsIndex
private itemCounter!: Models.ItemCounter
private navigationDisplayController!: Models.ItemDisplayController<Models.SNNote | Models.FileItem>
private tagDisplayController!: Models.ItemDisplayController<Models.SNTag>
private navigationDisplayController!: Models.ItemDisplayController<
Models.SNNote | Models.FileItem,
Models.NotesAndFilesDisplayOptions
>
private tagDisplayController!: Models.ItemDisplayController<Models.SNTag, Models.TagsDisplayOptions>
private itemsKeyDisplayController!: Models.ItemDisplayController<SNItemsKey>
private componentDisplayController!: Models.ItemDisplayController<Models.SNComponent>
private themeDisplayController!: Models.ItemDisplayController<Models.SNTheme>
@@ -52,11 +51,15 @@ export class ItemManager
this.unsubChangeObserver = this.payloadManager.addObserver(ContentType.Any, this.setPayloads.bind(this))
}
private rebuildSystemSmartViews(criteria: Models.FilterDisplayOptions): Models.SmartView[] {
private rebuildSystemSmartViews(criteria: Models.NotesAndFilesDisplayOptions): Models.SmartView[] {
this.systemSmartViews = Models.BuildSmartViews(criteria)
return this.systemSmartViews
}
public getCollection(): Models.ItemCollection {
return this.collection
}
private createCollection() {
this.collection = new Models.ItemCollection()
@@ -94,7 +97,7 @@ export class ItemManager
sortDirection: 'asc',
})
this.tagItemsIndex = new Models.TagItemsIndex(this.collection, this.tagItemsIndex?.observers)
this.itemCounter = new Models.ItemCounter(this.collection, this.itemCounter?.observers)
}
private get allDisplayControllers(): Models.ItemDisplayController<Models.DisplayItem>[] {
@@ -113,6 +116,10 @@ export class ItemManager
return this.collection.invalidElements()
}
public get invalidNonVaultedItems(): Models.EncryptedItemInterface[] {
return this.invalidItems.filter((item) => !item.key_system_identifier)
}
public createItemFromPayload(payload: Models.DecryptedPayloadInterface): Models.DecryptedItemInterface {
return Models.CreateDecryptedItemFromPayload(payload)
}
@@ -121,8 +128,8 @@ export class ItemManager
return new Models.DecryptedPayload(object)
}
public setPrimaryItemDisplayOptions(options: Models.DisplayOptions): void {
const override: Models.FilterDisplayOptions = {}
public setPrimaryItemDisplayOptions(options: Models.NotesAndFilesDisplayControllerOptions): void {
const override: Models.NotesAndFilesDisplayOptions = {}
const additionalFilters: Models.ItemFilter[] = []
if (options.views && options.views.find((view) => view.uuid === Models.SystemViewId.AllNotes)) {
@@ -164,7 +171,7 @@ export class ItemManager
})
.filter((view) => view != undefined)
const updatedOptions: Models.DisplayOptions = {
const updatedOptions: Models.DisplayControllerDisplayOptions & Models.NotesAndFilesDisplayOptions = {
...options,
...override,
...{
@@ -173,7 +180,7 @@ export class ItemManager
},
}
if (updatedOptions.sortBy === CollectionSort.Title) {
if (updatedOptions.sortBy === Models.CollectionSort.Title) {
updatedOptions.sortDirection = updatedOptions.sortDirection === 'asc' ? 'dsc' : 'asc'
}
@@ -181,6 +188,17 @@ export class ItemManager
customFilter: Models.computeUnifiedFilterForDisplayOptions(updatedOptions, this.collection, additionalFilters),
...updatedOptions,
})
this.itemCounter.setDisplayOptions(updatedOptions)
}
public setVaultDisplayOptions(options: Models.VaultDisplayOptions): void {
this.navigationDisplayController.setVaultDisplayOptions(options)
this.tagDisplayController.setVaultDisplayOptions(options)
this.smartViewDisplayController.setVaultDisplayOptions(options)
this.fileDisplayController.setVaultDisplayOptions(options)
this.itemCounter.setVaultDisplayOptions(options)
}
public getDisplayableNotes(): Models.SNNote[] {
@@ -214,7 +232,7 @@ export class ItemManager
;(this.unsubChangeObserver as unknown) = undefined
;(this.payloadManager as unknown) = undefined
;(this.collection as unknown) = undefined
;(this.tagItemsIndex as unknown) = undefined
;(this.itemCounter as unknown) = undefined
;(this.tagDisplayController as unknown) = undefined
;(this.navigationDisplayController as unknown) = undefined
;(this.itemsKeyDisplayController as unknown) = undefined
@@ -252,9 +270,6 @@ export class ItemManager
return this.findItem(uuid) as T
}
/**
* Returns all items matching given ids
*/
findItems<T extends Models.DecryptedItemInterface>(uuids: UuidString[]): T[] {
return this.collection.findAllDecrypted(uuids) as T[]
}
@@ -271,6 +286,7 @@ export class ItemManager
return this.collection.nondeletedElements().filter(Models.isDecryptedItem)
}
/** Unlock .items, this function includes error decrypting items */
allTrackedItems(): Models.ItemInterface[] {
return this.collection.all()
}
@@ -280,26 +296,26 @@ export class ItemManager
}
public addNoteCountChangeObserver(observer: Models.TagItemCountChangeObserver): () => void {
return this.tagItemsIndex.addCountChangeObserver(observer)
return this.itemCounter.addCountChangeObserver(observer)
}
public allCountableNotesCount(): number {
return this.tagItemsIndex.allCountableNotesCount()
return this.itemCounter.allCountableNotesCount()
}
public allCountableFilesCount(): number {
return this.tagItemsIndex.allCountableFilesCount()
return this.itemCounter.allCountableFilesCount()
}
public countableNotesForTag(tag: Models.SNTag | Models.SmartView): number {
if (tag instanceof Models.SmartView) {
if (tag.uuid === Models.SystemViewId.AllNotes) {
return this.tagItemsIndex.allCountableNotesCount()
return this.itemCounter.allCountableNotesCount()
}
throw Error('countableItemsForTag is not meant to be used for smart views.')
}
return this.tagItemsIndex.countableItemsForTag(tag)
return this.itemCounter.countableItemsForTag(tag)
}
public getNoteCount(): number {
@@ -330,12 +346,12 @@ export class ItemManager
/**
* Returns the items that reference the given item, or an empty array if no results.
*/
public itemsReferencingItem(
itemToLookupUuidFor: Models.DecryptedItemInterface,
public itemsReferencingItem<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface>(
itemToLookupUuidFor: { uuid: UuidString },
contentType?: ContentType,
): Models.DecryptedItemInterface[] {
): I[] {
const uuids = this.collection.uuidsThatReferenceUuid(itemToLookupUuidFor.uuid)
let referencing = this.findItems(uuids)
let referencing = this.findItems<I>(uuids)
if (contentType) {
referencing = referencing.filter((ref) => {
return ref?.content_type === contentType
@@ -405,7 +421,7 @@ export class ItemManager
}
this.collection.onChange(delta)
this.tagItemsIndex.onChange(delta)
this.itemCounter.onChange(delta)
const affectedContentTypesArray = Array.from(affectedContentTypes.values())
for (const controller of this.allDisplayControllers) {
@@ -509,250 +525,6 @@ export class ItemManager
}
}
/**
* Consumers wanting to modify an item should run it through this block,
* so that data is properly mapped through our function, and latest state
* is properly reconciled.
*/
public async changeItem<
M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator,
I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface,
>(
itemToLookupUuidFor: I,
mutate?: (mutator: M) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<I> {
const results = await this.changeItems<M, I>(
[itemToLookupUuidFor],
mutate,
mutationType,
emitSource,
payloadSourceKey,
)
return results[0]
}
/**
* @param mutate If not supplied, the intention would simply be to mark the item as dirty.
*/
public async changeItems<
M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator,
I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface,
>(
itemsToLookupUuidsFor: I[],
mutate?: (mutator: M) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<I[]> {
const items = this.findItemsIncludingBlanks(Uuids(itemsToLookupUuidsFor))
const payloads: Models.DecryptedPayloadInterface[] = []
for (const item of items) {
if (!item) {
throw Error('Attempting to change non-existant item')
}
const mutator = Models.CreateDecryptedMutatorForItem(item, mutationType)
if (mutate) {
mutate(mutator as M)
}
const payload = mutator.getResult()
payloads.push(payload)
}
await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey)
const results = this.findItems(payloads.map((p) => p.uuid)) as I[]
return results
}
/**
* Run unique mutations per each item in the array, then only propagate all changes
* once all mutations have been run. This differs from `changeItems` in that changeItems
* runs the same mutation on all items.
*/
public async runTransactionalMutations(
transactions: Models.TransactionalMutation[],
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<(Models.DecryptedItemInterface | undefined)[]> {
const payloads: Models.DecryptedPayloadInterface[] = []
for (const transaction of transactions) {
const item = this.findItem(transaction.itemUuid)
if (!item) {
continue
}
const mutator = Models.CreateDecryptedMutatorForItem(
item,
transaction.mutationType || Models.MutationType.UpdateUserTimestamps,
)
transaction.mutate(mutator)
const payload = mutator.getResult()
payloads.push(payload)
}
await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey)
const results = this.findItems(payloads.map((p) => p.uuid))
return results
}
public async runTransactionalMutation(
transaction: Models.TransactionalMutation,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.DecryptedItemInterface | undefined> {
const item = this.findSureItem(transaction.itemUuid)
const mutator = Models.CreateDecryptedMutatorForItem(
item,
transaction.mutationType || Models.MutationType.UpdateUserTimestamps,
)
transaction.mutate(mutator)
const payload = mutator.getResult()
await this.payloadManager.emitPayloads([payload], emitSource, payloadSourceKey)
const result = this.findItem(payload.uuid)
return result
}
async changeNote(
itemToLookupUuidFor: Models.SNNote,
mutate: (mutator: Models.NoteMutator) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.DecryptedPayloadInterface[]> {
const note = this.findItem<Models.SNNote>(itemToLookupUuidFor.uuid)
if (!note) {
throw Error('Attempting to change non-existant note')
}
const mutator = new Models.NoteMutator(note, mutationType)
return this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
}
async changeTag(
itemToLookupUuidFor: Models.SNTag,
mutate: (mutator: Models.TagMutator) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.SNTag> {
const tag = this.findItem<Models.SNTag>(itemToLookupUuidFor.uuid)
if (!tag) {
throw Error('Attempting to change non-existant tag')
}
const mutator = new Models.TagMutator(tag, mutationType)
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
return this.findSureItem<Models.SNTag>(itemToLookupUuidFor.uuid)
}
async changeComponent(
itemToLookupUuidFor: Models.SNComponent,
mutate: (mutator: Models.ComponentMutator) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.SNComponent> {
const component = this.findItem<Models.SNComponent>(itemToLookupUuidFor.uuid)
if (!component) {
throw Error('Attempting to change non-existant component')
}
const mutator = new Models.ComponentMutator(component, mutationType)
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
return this.findSureItem<Models.SNComponent>(itemToLookupUuidFor.uuid)
}
async changeFeatureRepo(
itemToLookupUuidFor: Models.SNFeatureRepo,
mutate: (mutator: Models.FeatureRepoMutator) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.SNFeatureRepo> {
const repo = this.findItem(itemToLookupUuidFor.uuid)
if (!repo) {
throw Error('Attempting to change non-existant repo')
}
const mutator = new Models.FeatureRepoMutator(repo, mutationType)
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
return this.findSureItem<Models.SNFeatureRepo>(itemToLookupUuidFor.uuid)
}
async changeActionsExtension(
itemToLookupUuidFor: Models.SNActionsExtension,
mutate: (mutator: Models.ActionsExtensionMutator) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.SNActionsExtension> {
const extension = this.findItem<Models.SNActionsExtension>(itemToLookupUuidFor.uuid)
if (!extension) {
throw Error('Attempting to change non-existant extension')
}
const mutator = new Models.ActionsExtensionMutator(extension, mutationType)
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
return this.findSureItem<Models.SNActionsExtension>(itemToLookupUuidFor.uuid)
}
async changeItemsKey(
itemToLookupUuidFor: Models.ItemsKeyInterface,
mutate: (mutator: Models.ItemsKeyMutatorInterface) => void,
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.ItemsKeyInterface> {
const itemsKey = this.findItem<SNItemsKey>(itemToLookupUuidFor.uuid)
if (!itemsKey) {
throw Error('Attempting to change non-existant itemsKey')
}
const mutator = new ItemsKeyMutator(itemsKey, mutationType)
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
return this.findSureItem<Models.ItemsKeyInterface>(itemToLookupUuidFor.uuid)
}
private async applyTransform<T extends Models.DecryptedItemMutator>(
mutator: T,
mutate: (mutator: T) => void,
emitSource = Models.PayloadEmitSource.LocalChanged,
payloadSourceKey?: string,
): Promise<Models.DecryptedPayloadInterface[]> {
mutate(mutator)
const payload = mutator.getResult()
return this.payloadManager.emitPayload(payload, emitSource, payloadSourceKey)
}
/**
* Sets the item as needing sync. The item is then run through the mapping function,
* and propagated to mapping observers.
* @param isUserModified - Whether to update the item's "user modified date"
*/
public async setItemDirty(itemToLookupUuidFor: Models.DecryptedItemInterface, isUserModified = false) {
const result = await this.setItemsDirty([itemToLookupUuidFor], isUserModified)
return result[0]
}
public async setItemsDirty(
itemsToLookupUuidsFor: Models.DecryptedItemInterface[],
isUserModified = false,
): Promise<Models.DecryptedItemInterface[]> {
return this.changeItems(
itemsToLookupUuidsFor,
undefined,
isUserModified ? Models.MutationType.UpdateUserTimestamps : Models.MutationType.NoUpdateUserTimestamps,
)
}
/**
* Returns an array of items that need to be synced.
*/
@@ -760,47 +532,6 @@ export class ItemManager
return this.collection.dirtyElements().filter(Models.isDecryptedOrDeletedItem)
}
/**
* Duplicates an item and maps it, thus propagating the item to observers.
* @param isConflict - Whether to mark the duplicate as a conflict of the original.
*/
public async duplicateItem<T extends Models.DecryptedItemInterface>(
itemToLookupUuidFor: T,
isConflict = false,
additionalContent?: Partial<Models.ItemContent>,
) {
const item = this.findSureItem(itemToLookupUuidFor.uuid)
const payload = item.payload.copy()
const resultingPayloads = Models.PayloadsByDuplicating({
payload,
baseCollection: this.payloadManager.getMasterCollection(),
isConflict,
additionalContent,
})
await this.payloadManager.emitPayloads(resultingPayloads, Models.PayloadEmitSource.LocalChanged)
const duplicate = this.findSureItem<T>(resultingPayloads[0].uuid)
return duplicate
}
public async createItem<T extends Models.DecryptedItemInterface, C extends Models.ItemContent = Models.ItemContent>(
contentType: ContentType,
content: C,
needsSync = false,
): Promise<T> {
const payload = new Models.DecryptedPayload<C>({
uuid: UuidGenerator.GenerateUuid(),
content_type: contentType,
content: Models.FillItemContent<C>(content),
dirty: needsSync,
...Models.PayloadTimestampDefaults(),
})
await this.payloadManager.emitPayload(payload, Models.PayloadEmitSource.LocalInserted)
return this.findSureItem<T>(payload.uuid)
}
public createTemplateItem<
C extends Models.ItemContent = Models.ItemContent,
I extends Models.DecryptedItemInterface<C> = Models.DecryptedItemInterface<C>,
@@ -824,75 +555,6 @@ export class ItemManager
return !this.findItem(item.uuid)
}
public async insertItem(item: Models.DecryptedItemInterface): Promise<Models.DecryptedItemInterface> {
return this.emitItemFromPayload(item.payload, Models.PayloadEmitSource.LocalChanged)
}
public async insertItems(
items: Models.DecryptedItemInterface[],
emitSource: Models.PayloadEmitSource = Models.PayloadEmitSource.LocalInserted,
): Promise<Models.DecryptedItemInterface[]> {
return this.emitItemsFromPayloads(
items.map((item) => item.payload),
emitSource,
)
}
public async emitItemFromPayload(
payload: Models.DecryptedPayloadInterface,
emitSource: Models.PayloadEmitSource,
): Promise<Models.DecryptedItemInterface> {
await this.payloadManager.emitPayload(payload, emitSource)
return this.findSureItem(payload.uuid)
}
public async emitItemsFromPayloads(
payloads: Models.DecryptedPayloadInterface[],
emitSource: Models.PayloadEmitSource,
): Promise<Models.DecryptedItemInterface[]> {
await this.payloadManager.emitPayloads(payloads, emitSource)
const uuids = Uuids(payloads)
return this.findItems(uuids)
}
public async setItemToBeDeleted(
itemToLookupUuidFor: Models.DecryptedItemInterface | Models.EncryptedItemInterface,
source: Models.PayloadEmitSource = Models.PayloadEmitSource.LocalChanged,
): Promise<void> {
const referencingIdsCapturedBeforeChanges = this.collection.uuidsThatReferenceUuid(itemToLookupUuidFor.uuid)
const item = this.findAnyItem(itemToLookupUuidFor.uuid)
if (!item) {
return
}
const mutator = new Models.DeleteItemMutator(item, Models.MutationType.UpdateUserTimestamps)
const deletedPayload = mutator.getDeletedResult()
await this.payloadManager.emitPayload(deletedPayload, source)
for (const referencingId of referencingIdsCapturedBeforeChanges) {
const referencingItem = this.findItem(referencingId)
if (referencingItem) {
await this.changeItem(referencingItem, (mutator) => {
mutator.removeItemAsRelationship(item)
})
}
}
}
public async setItemsToBeDeleted(
itemsToLookupUuidsFor: (Models.DecryptedItemInterface | Models.EncryptedItemInterface)[],
): Promise<void> {
await Promise.all(itemsToLookupUuidsFor.map((item) => this.setItemToBeDeleted(item)))
}
public getItems<T extends Models.DecryptedItemInterface>(contentType: ContentType | ContentType[]): T[] {
return this.collection.allDecrypted<T>(contentType)
}
@@ -1018,20 +680,6 @@ export class ItemManager
return chain
}
public async findOrCreateTagParentChain(titlesHierarchy: string[]): Promise<Models.SNTag> {
let current: Models.SNTag | undefined = undefined
for (const title of titlesHierarchy) {
current = await this.findOrCreateTagByTitle(title, current)
}
if (!current) {
throw new Error('Invalid tag hierarchy')
}
return current
}
public getTagChildren(itemToLookupUuidFor: Models.SNTag): Models.SNTag[] {
const tag = this.findItem<Models.SNTag>(itemToLookupUuidFor.uuid)
if (!tag) {
@@ -1079,117 +727,12 @@ export class ItemManager
return true
}
/**
* @returns The changed child tag
*/
public setTagParent(parentTag: Models.SNTag, childTag: Models.SNTag): Promise<Models.SNTag> {
if (parentTag.uuid === childTag.uuid) {
throw new Error('Can not set a tag parent of itself')
}
if (this.isTagAncestor(childTag, parentTag)) {
throw new Error('Can not set a tag ancestor of itself')
}
return this.changeTag(childTag, (m) => {
m.makeChildOf(parentTag)
})
}
/**
* @returns The changed child tag
*/
public unsetTagParent(childTag: Models.SNTag): Promise<Models.SNTag> {
const parentTag = this.getTagParent(childTag)
if (!parentTag) {
return Promise.resolve(childTag)
}
return this.changeTag(childTag, (m) => {
m.unsetParent()
})
}
public async associateFileWithNote(file: Models.FileItem, note: Models.SNNote): Promise<Models.FileItem> {
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
mutator.addNote(note)
})
}
public async disassociateFileWithNote(file: Models.FileItem, note: Models.SNNote): Promise<Models.FileItem> {
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
mutator.removeNote(note)
})
}
public async addTagToNote(note: Models.SNNote, tag: Models.SNTag, addHierarchy: boolean): Promise<Models.SNTag[]> {
let tagsToAdd = [tag]
if (addHierarchy) {
const parentChainTags = this.getTagParentChain(tag)
tagsToAdd = [...parentChainTags, tag]
}
return Promise.all(
tagsToAdd.map((tagToAdd) => {
return this.changeTag(tagToAdd, (mutator) => {
mutator.addNote(note)
}) as Promise<Models.SNTag>
}),
)
}
public async addTagToFile(file: Models.FileItem, tag: Models.SNTag, addHierarchy: boolean): Promise<Models.SNTag[]> {
let tagsToAdd = [tag]
if (addHierarchy) {
const parentChainTags = this.getTagParentChain(tag)
tagsToAdd = [...parentChainTags, tag]
}
return Promise.all(
tagsToAdd.map((tagToAdd) => {
return this.changeTag(tagToAdd, (mutator) => {
mutator.addFile(file)
}) as Promise<Models.SNTag>
}),
)
}
public async linkNoteToNote(note: Models.SNNote, otherNote: Models.SNNote): Promise<Models.SNNote> {
return this.changeItem<Models.NoteMutator, Models.SNNote>(note, (mutator) => {
mutator.addNote(otherNote)
})
}
public async linkFileToFile(file: Models.FileItem, otherFile: Models.FileItem): Promise<Models.FileItem> {
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
mutator.addFile(otherFile)
})
}
public async unlinkItems(itemA: DecryptedItemInterface<ItemContent>, itemB: DecryptedItemInterface<ItemContent>) {
const relationshipDirection = this.relationshipDirectionBetweenItems(itemA, itemB)
if (relationshipDirection === ItemRelationshipDirection.NoRelationship) {
throw new Error('Trying to unlink already unlinked items')
}
const itemToChange = relationshipDirection === ItemRelationshipDirection.AReferencesB ? itemA : itemB
const itemToRemove = itemToChange === itemA ? itemB : itemA
return this.changeItem(itemToChange, (mutator) => {
mutator.removeItemAsRelationship(itemToRemove)
})
}
/**
* Get tags for a note sorted in natural order
* @param item - The item whose tags will be returned
* @returns Array containing tags associated with an item
*/
public getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): Models.SNTag[] {
public getSortedTagsForItem(item: Models.DecryptedItemInterface<Models.ItemContent>): Models.SNTag[] {
return naturalSort(
this.itemsReferencingItem(item).filter((ref) => {
return ref?.content_type === ContentType.Tag
@@ -1198,81 +741,16 @@ export class ItemManager
)
}
public async createTag(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise<Models.SNTag> {
const newTag = await this.createItem<Models.SNTag>(
ContentType.Tag,
Models.FillItemContent<Models.TagContent>({ title }),
true,
)
if (parentItemToLookupUuidFor) {
const parentTag = this.findItem<Models.SNTag>(parentItemToLookupUuidFor.uuid)
if (!parentTag) {
throw new Error('Invalid parent tag')
}
return this.changeTag(newTag, (m) => {
m.makeChildOf(parentTag)
})
}
return newTag
}
public async createSmartView<T extends Models.DecryptedItemInterface>(
title: string,
predicate: Models.PredicateInterface<T>,
iconString?: string,
): Promise<Models.SmartView> {
return this.createItem(
ContentType.SmartView,
Models.FillItemContent({
title,
predicate: predicate.toJson(),
iconString: iconString || SmartViewDefaultIconName,
} as Models.SmartViewContent),
true,
) as Promise<Models.SmartView>
}
public async createSmartViewFromDSL<T extends Models.DecryptedItemInterface>(dsl: string): Promise<Models.SmartView> {
let components = null
try {
components = JSON.parse(dsl.substring(1, dsl.length))
} catch (e) {
throw Error('Invalid smart view syntax')
}
const title = components[0]
const predicate = Models.predicateFromDSLString<T>(dsl)
return this.createSmartView(title, predicate)
}
public async createTagOrSmartView(title: string): Promise<Models.SNTag | Models.SmartView> {
if (this.isSmartViewTitle(title)) {
return this.createSmartViewFromDSL(title)
} else {
return this.createTag(title)
}
}
public isSmartViewTitle(title: string): boolean {
return title.startsWith(Models.SMART_TAG_DSL_PREFIX)
}
/**
* Finds or creates a tag with a given title
*/
public async findOrCreateTagByTitle(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise<Models.SNTag> {
const tag = this.findTagByTitleAndParent(title, parentItemToLookupUuidFor)
return tag || this.createTag(title, parentItemToLookupUuidFor)
}
public notesMatchingSmartView(view: Models.SmartView): Models.SNNote[] {
const criteria: Models.FilterDisplayOptions = {
const criteria: Models.NotesAndFilesDisplayOptions = {
views: [view],
}
return Models.itemsMatchingOptions(
return Models.notesAndFilesMatchingOptions(
criteria,
this.collection.allDecrypted(ContentType.Note),
this.collection,
@@ -1299,14 +777,6 @@ export class ItemManager
return this.notesMatchingSmartView(this.trashSmartView)
}
/**
* Permanently deletes any items currently in the trash. Consumer must manually call sync.
*/
public async emptyTrash(): Promise<void> {
const notes = this.trashedItems
await this.setItemsToBeDeleted(notes)
}
/**
* Returns all smart views, sorted by title.
*/
@@ -1346,53 +816,29 @@ export class ItemManager
this.payloadManager.resetState()
}
public removeItemLocally(item: Models.DecryptedItemInterface | Models.DeletedItemInterface): void {
this.collection.discard([item])
this.payloadManager.removePayloadLocally(item.payload)
/**
* Important: Caller must coordinate with storage service separately to delete item from persistent database.
*/
public removeItemLocally(item: Models.AnyItemInterface): void {
this.removeItemsLocally([item])
}
const delta = Models.CreateItemDelta({ discarded: [item] as Models.DeletedItemInterface[] })
/**
* Important: Caller must coordinate with storage service separately to delete item from persistent database.
*/
public removeItemsLocally(items: Models.AnyItemInterface[]): void {
this.collection.discard(items)
this.payloadManager.removePayloadLocally(items.map((item) => item.payload))
const delta = Models.CreateItemDelta({ discarded: items as Models.DeletedItemInterface[] })
const affectedContentTypes = items.map((item) => item.content_type)
for (const controller of this.allDisplayControllers) {
if (controller.contentTypes.some((ct) => ct === item.content_type)) {
if (controller.contentTypes.some((ct) => affectedContentTypes.includes(ct))) {
controller.onCollectionChange(delta)
}
}
}
public renameFile(file: Models.FileItem, name: string): Promise<Models.FileItem> {
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
mutator.name = name
})
}
public async setLastSyncBeganForItems(
itemsToLookupUuidsFor: (Models.DecryptedItemInterface | Models.DeletedItemInterface)[],
date: Date,
globalDirtyIndex: number,
): Promise<(Models.DecryptedItemInterface | Models.DeletedItemInterface)[]> {
const uuids = Uuids(itemsToLookupUuidsFor)
const items = this.collection.findAll(uuids).filter(Models.isDecryptedOrDeletedItem)
const payloads: (Models.DecryptedPayloadInterface | Models.DeletedPayloadInterface)[] = []
for (const item of items) {
const mutator = new Models.ItemMutator<Models.DecryptedPayloadInterface | Models.DeletedPayloadInterface>(
item,
Models.MutationType.NonDirtying,
)
mutator.setBeginSync(date, globalDirtyIndex)
const payload = mutator.getResult()
payloads.push(payload)
}
await this.payloadManager.emitPayloads(payloads, Models.PayloadEmitSource.PreSyncSave)
return this.findAnyItems(uuids) as (Models.DecryptedItemInterface | Models.DeletedItemInterface)[]
}
public relationshipDirectionBetweenItems(
itemA: Models.DecryptedItemInterface<Models.ItemContent>,
itemB: Models.DecryptedItemInterface<Models.ItemContent>,
@@ -1407,12 +853,8 @@ export class ItemManager
: ItemRelationshipDirection.NoRelationship
}
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
return Promise.resolve({
items: {
allIds: Uuids(this.collection.all()),
},
})
itemsBelongingToKeySystem(systemIdentifier: Models.KeySystemIdentifier): Models.DecryptedItemInterface[] {
return this.items.filter((item) => item.key_system_identifier === systemIdentifier)
}
public conflictsOf(uuid: string) {
@@ -1422,4 +864,8 @@ export class ItemManager
public numberOfNotesWithConflicts(): number {
return this.findItems(this.collection.uuidsOfItemsWithConflicts()).filter(Models.isNote).length
}
getNoteLinkedFiles(note: Models.SNNote): Models.FileItem[] {
return this.itemsReferencingItem(note).filter(Models.isFile)
}
}