feat: add models package
This commit is contained in:
10
packages/models/src/Domain/Utilities/Item/FindItem.ts
Normal file
10
packages/models/src/Domain/Utilities/Item/FindItem.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
|
||||
|
||||
export function FindItem<I extends ItemInterface = ItemInterface>(items: I[], uuid: Uuid): I | undefined {
|
||||
return items.find((item) => item.uuid === uuid)
|
||||
}
|
||||
|
||||
export function SureFindItem<I extends ItemInterface = ItemInterface>(items: I[], uuid: Uuid): I {
|
||||
return FindItem(items, uuid) as I
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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<C extends ItemContent = ItemContent>(
|
||||
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<C> = 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<Partial<C>>(leftContentCopy, keysToIgnore)
|
||||
|
||||
const rightContentCopy: Partial<C> = 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<Partial<C>>(rightContentCopy, keysToIgnore)
|
||||
|
||||
return JSON.stringify(leftContentCopy) === JSON.stringify(rightContentCopy)
|
||||
}
|
||||
113
packages/models/src/Domain/Utilities/Item/ItemGenerator.ts
Normal file
113
packages/models/src/Domain/Utilities/Item/ItemGenerator.ts
Normal file
@@ -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<C extends ItemContent = ItemContent> = new (payload: DecryptedPayloadInterface<C>) => DecryptedItem<C>
|
||||
|
||||
type MutatorClass<C extends ItemContent = ItemContent> = new (
|
||||
item: DecryptedItemInterface<C>,
|
||||
type: MutationType,
|
||||
) => DecryptedItemMutator<C>
|
||||
|
||||
type MappingEntry<C extends ItemContent = ItemContent> = {
|
||||
itemClass: ItemClass<C>
|
||||
mutatorClass?: MutatorClass<C>
|
||||
}
|
||||
|
||||
const ContentTypeClassMapping: Partial<Record<ContentType, MappingEntry>> = {
|
||||
[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<Record<ContentType, MappingEntry>>
|
||||
|
||||
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<C> = DecryptedItemMutator<C>,
|
||||
>(contentType: ContentType, itemClass: ItemClass<C>, mutatorClass: M) {
|
||||
const entry: MappingEntry<C> = {
|
||||
itemClass: itemClass,
|
||||
mutatorClass: mutatorClass as unknown as MutatorClass<C>,
|
||||
}
|
||||
ContentTypeClassMapping[contentType] = entry as unknown as MappingEntry<ItemContent>
|
||||
}
|
||||
|
||||
export function CreateDecryptedItemFromPayload<
|
||||
C extends ItemContent = ItemContent,
|
||||
T extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(payload: DecryptedPayloadInterface<C>): 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<C> = DecryptedItemInterface<C>,
|
||||
>(
|
||||
payload: DecryptedPayloadInterface<C> | EncryptedPayloadInterface | DeletedPayloadInterface,
|
||||
): EncryptedItemInterface | DeletedItemInterface | T {
|
||||
if (isDecryptedPayload(payload)) {
|
||||
return CreateDecryptedItemFromPayload<C, T>(payload)
|
||||
} else if (isEncryptedPayload(payload)) {
|
||||
return new EncryptedItem(payload)
|
||||
} else if (isDeletedPayload(payload)) {
|
||||
return new DeletedItem(payload)
|
||||
} else {
|
||||
throw Error('Unhandled case in CreateItemFromPayload')
|
||||
}
|
||||
}
|
||||
@@ -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<FullyFormedPayloadInterface>,
|
||||
) => SyncResolvedPayload[]
|
||||
|
||||
const NoteDuplicationAffectedPayloads: AffectorFunction = (
|
||||
basePayload: FullyFormedPayloadInterface,
|
||||
duplicatePayload: FullyFormedPayloadInterface,
|
||||
baseCollection: ImmutablePayloadCollection<FullyFormedPayloadInterface>,
|
||||
) => {
|
||||
/** If note has editor, maintain editor relationship in duplicate note */
|
||||
const components = baseCollection
|
||||
.all(ContentType.Component)
|
||||
.filter(isDecryptedPayload)
|
||||
.map((payload) => {
|
||||
return CreateDecryptedItemFromPayload<ComponentContent, SNComponent>(
|
||||
payload as DecryptedPayloadInterface<ComponentContent>,
|
||||
)
|
||||
})
|
||||
|
||||
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<Record<ContentType, AffectorFunction>>
|
||||
@@ -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> = T extends DecryptedTransferPayload<infer C>
|
||||
? DecryptedPayloadInterface<C>
|
||||
: T extends EncryptedTransferPayload
|
||||
? EncryptedPayloadInterface
|
||||
: DeletedPayloadInterface
|
||||
|
||||
export type ConditionalTransferPayloadType<P> = P extends DecryptedPayloadInterface<infer C>
|
||||
? DecryptedTransferPayload<C>
|
||||
: P extends EncryptedPayloadInterface
|
||||
? EncryptedTransferPayload
|
||||
: DeletedTransferPayload
|
||||
@@ -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<C extends ItemContent = ItemContent>(
|
||||
payload: DecryptedPayloadInterface<C>,
|
||||
contentOverride: Partial<C>,
|
||||
): DecryptedPayloadInterface<C> {
|
||||
const params: DecryptedTransferPayload<C> = {
|
||||
...payload.ejected(),
|
||||
content: {
|
||||
...payload.content,
|
||||
...contentOverride,
|
||||
},
|
||||
}
|
||||
const result = CreatePayload(params, payload.source)
|
||||
return result
|
||||
}
|
||||
@@ -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<T extends FullyFormedTransferPayload>(
|
||||
from: T,
|
||||
source: PayloadSource,
|
||||
): ConditionalPayloadType<T> {
|
||||
if (isDecryptedTransferPayload(from)) {
|
||||
return new DecryptedPayload(from, source) as unknown as ConditionalPayloadType<T>
|
||||
} else if (isEncryptedTransferPayload(from)) {
|
||||
return new EncryptedPayload(from, source) as unknown as ConditionalPayloadType<T>
|
||||
} else if (isDeletedTransferPayload(from)) {
|
||||
return new DeletedPayload(from, source) as unknown as ConditionalPayloadType<T>
|
||||
} else {
|
||||
throw Error('Unhandled case in CreatePayload')
|
||||
}
|
||||
}
|
||||
10
packages/models/src/Domain/Utilities/Payload/FindPayload.ts
Normal file
10
packages/models/src/Domain/Utilities/Payload/FindPayload.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { PayloadInterface } from '../../Abstract/Payload/Interfaces/PayloadInterface'
|
||||
|
||||
export function FindPayload<P extends PayloadInterface = PayloadInterface>(payloads: P[], uuid: Uuid): P | undefined {
|
||||
return payloads.find((payload) => payload.uuid === uuid)
|
||||
}
|
||||
|
||||
export function SureFindPayload<P extends PayloadInterface = PayloadInterface>(payloads: P[], uuid: Uuid): P {
|
||||
return FindPayload(payloads, uuid) as P
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
98
packages/models/src/Domain/Utilities/Payload/PayloadSplit.ts
Normal file
98
packages/models/src/Domain/Utilities/Payload/PayloadSplit.ts
Normal file
@@ -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<C extends ItemContent = ItemContent> {
|
||||
encrypted: EncryptedPayloadInterface[]
|
||||
decrypted: DecryptedPayloadInterface<C>[]
|
||||
deleted: DeletedPayloadInterface[]
|
||||
}
|
||||
|
||||
export interface PayloadSplitWithDiscardables<C extends ItemContent = ItemContent> {
|
||||
encrypted: EncryptedPayloadInterface[]
|
||||
decrypted: DecryptedPayloadInterface<C>[]
|
||||
deleted: DeletedPayloadInterface[]
|
||||
discardable: DeletedPayloadInterface[]
|
||||
}
|
||||
|
||||
export interface NonDecryptedPayloadSplit {
|
||||
encrypted: EncryptedPayloadInterface[]
|
||||
deleted: DeletedPayloadInterface[]
|
||||
}
|
||||
|
||||
export function CreatePayloadSplit<C extends ItemContent = ItemContent>(
|
||||
payloads: FullyFormedPayloadInterface<C>[],
|
||||
): PayloadSplit<C> {
|
||||
const split: PayloadSplit<C> = {
|
||||
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<C extends ItemContent = ItemContent>(
|
||||
payloads: FullyFormedPayloadInterface<C>[],
|
||||
): PayloadSplitWithDiscardables<C> {
|
||||
const split: PayloadSplitWithDiscardables<C> = {
|
||||
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
|
||||
}
|
||||
@@ -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<P extends DecryptedPayloadInterface = DecryptedPayloadInterface>(
|
||||
payload: P,
|
||||
baseCollection: ImmutablePayloadCollection<FullyFormedPayloadInterface>,
|
||||
): 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
|
||||
}
|
||||
@@ -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<C extends ItemContent = ItemContent>(dto: {
|
||||
payload: FullyFormedPayloadInterface<C>
|
||||
baseCollection: ImmutablePayloadCollection<FullyFormedPayloadInterface>
|
||||
isConflict?: boolean
|
||||
additionalContent?: Partial<C>
|
||||
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
|
||||
}
|
||||
@@ -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<FullyFormedPayloadInterface>,
|
||||
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
|
||||
}
|
||||
80
packages/models/src/Domain/Utilities/Test/SpecUtils.ts
Normal file
80
packages/models/src/Domain/Utilities/Test/SpecUtils.ts
Normal file
@@ -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<NoteContent>): SNNote => {
|
||||
return new SNNote(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
uuid: mockUuid(),
|
||||
content_type: ContentType.Note,
|
||||
content: FillItemContent({ ...payload }),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const createNoteWithContent = (content: Partial<NoteContent>, createdAt?: Date): SNNote => {
|
||||
return new SNNote(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
uuid: mockUuid(),
|
||||
content_type: ContentType.Note,
|
||||
content: FillItemContent<NoteContent>(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<TagContent>({ title }),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const createFile = (name = 'screenshot.png') => {
|
||||
return new FileItem(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
uuid: mockUuid(),
|
||||
content_type: ContentType.File,
|
||||
content: FillItemContent<FileContent>({ name }),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const pinnedContent = (): Partial<ItemContent> => {
|
||||
return {
|
||||
appData: {
|
||||
'org.standardnotes.sn': {
|
||||
pinned: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user