internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user