feat: add models package

This commit is contained in:
Karol Sójko
2022-07-05 20:47:11 +02:00
parent 60d1554ff7
commit b614c71e79
199 changed files with 8772 additions and 22 deletions

View File

@@ -0,0 +1,51 @@
import { Uuid } from '@standardnotes/common'
import { AppData, DefaultAppDomain } from '../Item/Types/DefaultAppDomain'
import { ContentReference } from '../Reference/ContentReference'
import { AppDataField } from '../Item/Types/AppDataField'
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SpecializedContent {}
export interface ItemContent {
references: ContentReference[]
conflict_of?: Uuid
protected?: boolean
trashed?: boolean
pinned?: boolean
archived?: boolean
locked?: boolean
appData?: AppData
}
/**
* Modifies the input object to fill in any missing required values from the
* content body.
*/
export function FillItemContent<C extends ItemContent = ItemContent>(content: Partial<C>): C {
if (!content.references) {
content.references = []
}
if (!content.appData) {
content.appData = {
[DefaultAppDomain]: {},
}
}
if (!content.appData[DefaultAppDomain]) {
content.appData[DefaultAppDomain] = {}
}
if (!content.appData[DefaultAppDomain][AppDataField.UserModifiedDate]) {
content.appData[DefaultAppDomain][AppDataField.UserModifiedDate] = `${new Date()}`
}
return content as C
}
export function FillItemContentSpecialized<S extends SpecializedContent, C extends ItemContent = ItemContent>(
content: S,
): C {
return FillItemContent(content)
}

View File

@@ -0,0 +1,60 @@
import { Uuid } from '@standardnotes/common'
import { ContextPayload } from './ContextPayload'
import { ItemContent } from '../Content/ItemContent'
import { DecryptedTransferPayload, EncryptedTransferPayload } from '../TransferPayload'
export interface BackupFileEncryptedContextualPayload extends ContextPayload {
auth_hash?: string
content: string
created_at_timestamp: number
created_at: Date
duplicate_of?: Uuid
enc_item_key: string
items_key_id: string | undefined
updated_at: Date
updated_at_timestamp: number
}
export interface BackupFileDecryptedContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
content: C
created_at_timestamp: number
created_at: Date
duplicate_of?: Uuid
updated_at: Date
updated_at_timestamp: number
}
export function CreateEncryptedBackupFileContextPayload(
fromPayload: EncryptedTransferPayload,
): BackupFileEncryptedContextualPayload {
return {
auth_hash: fromPayload.auth_hash,
content_type: fromPayload.content_type,
content: fromPayload.content,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: false,
duplicate_of: fromPayload.duplicate_of,
enc_item_key: fromPayload.enc_item_key,
items_key_id: fromPayload.items_key_id,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
}
}
export function CreateDecryptedBackupFileContextPayload(
fromPayload: DecryptedTransferPayload,
): BackupFileDecryptedContextualPayload {
return {
content_type: fromPayload.content_type,
content: fromPayload.content,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: false,
duplicate_of: fromPayload.duplicate_of,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
}
}

View File

@@ -0,0 +1,24 @@
import { ItemContent } from '../Content/ItemContent'
import { DecryptedTransferPayload } from '../TransferPayload'
import { ContextPayload } from './ContextPayload'
/**
* Represents a payload with permissible fields for when a
* component wants to create a new item
*/
export interface ComponentCreateContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
content: C
created_at?: Date
}
export function createComponentCreatedContextPayload(
fromPayload: DecryptedTransferPayload,
): ComponentCreateContextualPayload {
return {
content_type: fromPayload.content_type,
content: fromPayload.content,
created_at: fromPayload.created_at,
deleted: false,
uuid: fromPayload.uuid,
}
}

View File

@@ -0,0 +1,24 @@
import { ItemContent } from '../Content/ItemContent'
import { DecryptedTransferPayload } from '../TransferPayload'
import { ContextPayload } from './ContextPayload'
/**
* Represents a payload with permissible fields for when a
* payload is retrieved from a component for saving
*/
export interface ComponentRetrievedContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
content: C
created_at?: Date
}
export function CreateComponentRetrievedContextPayload(
fromPayload: DecryptedTransferPayload,
): ComponentRetrievedContextualPayload {
return {
content_type: fromPayload.content_type,
content: fromPayload.content,
created_at: fromPayload.created_at,
deleted: false,
uuid: fromPayload.uuid,
}
}

View File

@@ -0,0 +1,9 @@
import { ContentType } from '@standardnotes/common'
import { ItemContent } from '../Content/ItemContent'
export interface ContextPayload<C extends ItemContent = ItemContent> {
uuid: string
content_type: ContentType
content: C | string | undefined
deleted: boolean
}

View File

@@ -0,0 +1,25 @@
import { ServerItemResponse } from '@standardnotes/responses'
import { isCorruptTransferPayload, isEncryptedTransferPayload } from '../TransferPayload'
export interface FilteredServerItem extends ServerItemResponse {
__passed_filter__: true
}
export function CreateFilteredServerItem(item: ServerItemResponse): FilteredServerItem {
return {
...item,
__passed_filter__: true,
}
}
export function FilterDisallowedRemotePayloadsAndMap(payloads: ServerItemResponse[]): FilteredServerItem[] {
return payloads.filter(isRemotePayloadAllowed).map(CreateFilteredServerItem)
}
export function isRemotePayloadAllowed(payload: ServerItemResponse): boolean {
if (isCorruptTransferPayload(payload)) {
return false
}
return isEncryptedTransferPayload(payload) || payload.content == undefined
}

View File

@@ -0,0 +1,107 @@
import { Uuid } from '@standardnotes/common'
import { ContextPayload } from './ContextPayload'
import { ItemContent } from '../Content/ItemContent'
import { DecryptedPayloadInterface, DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload'
import { useBoolean } from '@standardnotes/utils'
import { EncryptedTransferPayload, isEncryptedTransferPayload } from '../TransferPayload'
export function isEncryptedLocalStoragePayload(
p: LocalStorageEncryptedContextualPayload | LocalStorageDecryptedContextualPayload,
): p is LocalStorageEncryptedContextualPayload {
return isEncryptedTransferPayload(p as EncryptedTransferPayload)
}
export interface LocalStorageEncryptedContextualPayload extends ContextPayload {
auth_hash?: string
auth_params?: unknown
content: string
deleted: false
created_at_timestamp: number
created_at: Date
dirty: boolean
duplicate_of: Uuid | undefined
enc_item_key: string
errorDecrypting: boolean
items_key_id: string | undefined
updated_at_timestamp: number
updated_at: Date
waitingForKey: boolean
}
export interface LocalStorageDecryptedContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
content: C
created_at_timestamp: number
created_at: Date
deleted: false
dirty: boolean
duplicate_of?: Uuid
updated_at_timestamp: number
updated_at: Date
}
export interface LocalStorageDeletedContextualPayload extends ContextPayload {
content: undefined
created_at_timestamp: number
created_at: Date
deleted: true
dirty: true
duplicate_of?: Uuid
updated_at_timestamp: number
updated_at: Date
}
export function CreateEncryptedLocalStorageContextPayload(
fromPayload: EncryptedPayloadInterface,
): LocalStorageEncryptedContextualPayload {
return {
auth_hash: fromPayload.auth_hash,
content_type: fromPayload.content_type,
content: fromPayload.content,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: false,
dirty: fromPayload.dirty != undefined ? fromPayload.dirty : false,
duplicate_of: fromPayload.duplicate_of,
enc_item_key: fromPayload.enc_item_key,
errorDecrypting: fromPayload.errorDecrypting,
items_key_id: fromPayload.items_key_id,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
waitingForKey: fromPayload.waitingForKey,
}
}
export function CreateDecryptedLocalStorageContextPayload(
fromPayload: DecryptedPayloadInterface,
): LocalStorageDecryptedContextualPayload {
return {
content_type: fromPayload.content_type,
content: fromPayload.content,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: false,
duplicate_of: fromPayload.duplicate_of,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
dirty: useBoolean(fromPayload.dirty, false),
}
}
export function CreateDeletedLocalStorageContextPayload(
fromPayload: DeletedPayloadInterface,
): LocalStorageDeletedContextualPayload {
return {
content_type: fromPayload.content_type,
content: undefined,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: true,
dirty: true,
duplicate_of: fromPayload.duplicate_of,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
}
}

View File

@@ -0,0 +1,41 @@
import { Uuid } from '@standardnotes/common'
import { ItemContent } from '../Content/ItemContent'
import { DecryptedPayloadInterface, DeletedPayloadInterface, isDecryptedPayload } from '../Payload'
import { ContextPayload } from './ContextPayload'
export interface OfflineSyncPushContextualPayload extends ContextPayload {
content: ItemContent | undefined
created_at_timestamp: number
created_at: Date
duplicate_of?: Uuid
updated_at_timestamp: number
updated_at: Date
}
export function CreateOfflineSyncPushContextPayload(
fromPayload: DecryptedPayloadInterface | DeletedPayloadInterface,
): OfflineSyncPushContextualPayload {
const base: OfflineSyncPushContextualPayload = {
content: undefined,
content_type: fromPayload.content_type,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: false,
duplicate_of: fromPayload.duplicate_of,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
}
if (isDecryptedPayload(fromPayload)) {
return {
...base,
content: fromPayload.content,
}
} else {
return {
...base,
deleted: fromPayload.deleted,
}
}
}

View File

@@ -0,0 +1,30 @@
import { ContentType } from '@standardnotes/common'
import { DecryptedPayloadInterface, DeletedPayloadInterface, isDeletedPayload } from '../Payload'
/**
* The saved sync item payload represents the payload we want to map
* when mapping saved_items from the server or local sync mechanism. We only want to map the
* updated_at value the server returns for the item, and basically
* nothing else.
*/
export interface OfflineSyncSavedContextualPayload {
content_type: ContentType
created_at_timestamp: number
deleted: boolean
updated_at_timestamp?: number
updated_at: Date
uuid: string
}
export function CreateOfflineSyncSavedPayload(
fromPayload: DecryptedPayloadInterface | DeletedPayloadInterface,
): OfflineSyncSavedContextualPayload {
return {
content_type: fromPayload.content_type,
created_at_timestamp: fromPayload.created_at_timestamp,
deleted: isDeletedPayload(fromPayload),
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
}
}

View File

@@ -0,0 +1,50 @@
import { Uuid } from '@standardnotes/common'
import { DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload'
import { ContextPayload } from './ContextPayload'
export interface ServerSyncPushContextualPayload extends ContextPayload {
auth_hash?: string
content: string | undefined
created_at_timestamp: number
created_at: Date
duplicate_of?: Uuid
enc_item_key?: string
items_key_id?: string
updated_at_timestamp: number
updated_at: Date
}
export function CreateEncryptedServerSyncPushPayload(
fromPayload: EncryptedPayloadInterface,
): ServerSyncPushContextualPayload {
return {
content_type: fromPayload.content_type,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: false,
duplicate_of: fromPayload.duplicate_of,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
content: fromPayload.content,
enc_item_key: fromPayload.enc_item_key,
items_key_id: fromPayload.items_key_id,
auth_hash: fromPayload.auth_hash,
}
}
export function CreateDeletedServerSyncPushPayload(
fromPayload: DeletedPayloadInterface,
): ServerSyncPushContextualPayload {
return {
content_type: fromPayload.content_type,
created_at_timestamp: fromPayload.created_at_timestamp,
created_at: fromPayload.created_at,
deleted: true,
duplicate_of: fromPayload.duplicate_of,
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
content: undefined,
}
}

View File

@@ -0,0 +1,31 @@
import { useBoolean } from '@standardnotes/utils'
import { FilteredServerItem } from './FilteredServerItem'
import { ContentType } from '@standardnotes/common'
/**
* The saved sync item payload represents the payload we want to map
* when mapping saved_items from the server. We only want to map the
* updated_at value the server returns for the item, and basically
* nothing else.
*/
export interface ServerSyncSavedContextualPayload {
content_type: ContentType
created_at_timestamp: number
created_at: Date
deleted: boolean
updated_at_timestamp: number
updated_at: Date
uuid: string
}
export function CreateServerSyncSavedPayload(from: FilteredServerItem): ServerSyncSavedContextualPayload {
return {
content_type: from.content_type,
created_at_timestamp: from.created_at_timestamp,
created_at: from.created_at,
deleted: useBoolean(from.deleted, false),
updated_at_timestamp: from.updated_at_timestamp,
updated_at: from.updated_at,
uuid: from.uuid,
}
}

View File

@@ -0,0 +1,7 @@
import { ItemContent } from '../Content/ItemContent'
import { ContextPayload } from './ContextPayload'
export interface SessionHistoryContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
content: C
updated_at: Date
}

View File

@@ -0,0 +1,10 @@
export * from './ComponentCreate'
export * from './ComponentRetrieved'
export * from './BackupFile'
export * from './LocalStorage'
export * from './OfflineSyncPush'
export * from './OfflineSyncSaved'
export * from './ServerSyncPush'
export * from './SessionHistory'
export * from './ServerSyncSaved'
export * from './FilteredServerItem'

View File

@@ -0,0 +1,122 @@
import { dateToLocalizedString, useBoolean } from '@standardnotes/utils'
import { Uuid } from '@standardnotes/common'
import { DecryptedTransferPayload } from './../../TransferPayload/Interfaces/DecryptedTransferPayload'
import { AppDataField } from '../Types/AppDataField'
import { ComponentDataDomain, DefaultAppDomain } from '../Types/DefaultAppDomain'
import { DecryptedItemInterface } from '../Interfaces/DecryptedItem'
import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload'
import { GenericItem } from './GenericItem'
import { ItemContent } from '../../Content/ItemContent'
import { ItemContentsEqual } from '../../../Utilities/Item/ItemContentsEqual'
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
import { ContentReference } from '../../Reference/ContentReference'
export class DecryptedItem<C extends ItemContent = ItemContent>
extends GenericItem<DecryptedPayloadInterface<C>>
implements DecryptedItemInterface<C>
{
public readonly conflictOf?: Uuid
public readonly protected: boolean = false
public readonly trashed: boolean = false
public readonly pinned: boolean = false
public readonly archived: boolean = false
public readonly locked: boolean = false
constructor(payload: DecryptedPayloadInterface<C>) {
super(payload)
this.conflictOf = payload.content.conflict_of
const userModVal = this.getAppDomainValueWithDefault(AppDataField.UserModifiedDate, this.serverUpdatedAt || 0)
this.userModifiedDate = new Date(userModVal as number | Date)
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
this.protected = useBoolean(this.payload.content.protected, false)
this.trashed = useBoolean(this.payload.content.trashed, false)
this.pinned = this.getAppDomainValueWithDefault(AppDataField.Pinned, false)
this.archived = this.getAppDomainValueWithDefault(AppDataField.Archived, false)
this.locked = this.getAppDomainValueWithDefault(AppDataField.Locked, false)
}
public static DefaultAppDomain() {
return DefaultAppDomain
}
get content() {
return this.payload.content
}
get references(): ContentReference[] {
return this.payload.content.references || []
}
public isReferencingItem(item: DecryptedItemInterface): boolean {
return this.references.find((r) => r.uuid === item.uuid) != undefined
}
/**
* Inside of content is a record called `appData` (which should have been called `domainData`).
* It was named `appData` as a way to indicate that it can house data for multiple apps.
* Each key of appData is a domain string, which was originally designed
* to allow for multiple 3rd party apps who share access to the same data to store data
* in an isolated location. This design premise is antiquited and no longer pursued,
* however we continue to use it as not to uncesesarily create a large data migration
* that would require users to sync all their data.
*
* domainData[DomainKey] will give you another Record<string, any>.
*
* Currently appData['org.standardnotes.sn'] returns an object of type AppData.
* And appData['org.standardnotes.sn.components] returns an object of type ComponentData
*/
public getDomainData(
domain: typeof ComponentDataDomain | typeof DefaultAppDomain,
): undefined | Record<string, unknown> {
const domainData = this.payload.content.appData
if (!domainData) {
return undefined
}
const data = domainData[domain]
return data
}
public getAppDomainValue<T>(key: AppDataField | PrefKey): T | undefined {
const appData = this.getDomainData(DefaultAppDomain)
return appData?.[key] as T
}
public getAppDomainValueWithDefault<T, D extends T>(key: AppDataField | PrefKey, defaultValue: D): T {
const appData = this.getDomainData(DefaultAppDomain)
return (appData?.[key] as T) || defaultValue
}
public override payloadRepresentation(override?: Partial<DecryptedTransferPayload<C>>): DecryptedPayloadInterface<C> {
return this.payload.copy(override)
}
/**
* During sync conflicts, when determing whether to create a duplicate for an item,
* we can omit keys that have no meaningful weight and can be ignored. For example,
* if one component has active = true and another component has active = false,
* it would be needless to duplicate them, so instead we ignore that value.
*/
public contentKeysToIgnoreWhenCheckingEquality<C extends ItemContent = ItemContent>(): (keyof C)[] {
return ['conflict_of']
}
/** Same as `contentKeysToIgnoreWhenCheckingEquality`, but keys inside appData[Item.AppDomain] */
public appDataContentKeysToIgnoreWhenCheckingEquality(): AppDataField[] {
return [AppDataField.UserModifiedDate]
}
public getContentCopy() {
return JSON.parse(JSON.stringify(this.content))
}
public isItemContentEqualWith(otherItem: DecryptedItemInterface) {
return ItemContentsEqual(
this.payload.content,
otherItem.payload.content,
this.contentKeysToIgnoreWhenCheckingEquality(),
this.appDataContentKeysToIgnoreWhenCheckingEquality(),
)
}
}

View File

@@ -0,0 +1,18 @@
import { GenericItem } from './GenericItem'
import { DeletedPayloadInterface } from '../../Payload'
import { DeletedItemInterface } from '../Interfaces/DeletedItem'
import { DeletedTransferPayload } from '../../TransferPayload'
export class DeletedItem extends GenericItem<DeletedPayloadInterface> implements DeletedItemInterface {
deleted: true
content: undefined
constructor(payload: DeletedPayloadInterface) {
super(payload)
this.deleted = true
}
public override payloadRepresentation(override?: Partial<DeletedTransferPayload>): DeletedPayloadInterface {
return this.payload.copy(override)
}
}

View File

@@ -0,0 +1,34 @@
import { EncryptedTransferPayload } from './../../TransferPayload/Interfaces/EncryptedTransferPayload'
import { EncryptedItemInterface } from '../Interfaces/EncryptedItem'
import { EncryptedPayloadInterface } from '../../Payload/Interfaces/EncryptedPayload'
import { GenericItem } from './GenericItem'
export class EncryptedItem extends GenericItem<EncryptedPayloadInterface> implements EncryptedItemInterface {
constructor(payload: EncryptedPayloadInterface) {
super(payload)
}
get version() {
return this.payload.version
}
public override payloadRepresentation(override?: Partial<EncryptedTransferPayload>): EncryptedPayloadInterface {
return this.payload.copy(override)
}
get errorDecrypting() {
return this.payload.errorDecrypting
}
get waitingForKey() {
return this.payload.waitingForKey
}
get content() {
return this.payload.content
}
get auth_hash() {
return this.payload.auth_hash
}
}

View File

@@ -0,0 +1,189 @@
import { ContentType, Uuid } from '@standardnotes/common'
import { dateToLocalizedString, deepFreeze } from '@standardnotes/utils'
import { TransferPayload } from './../../TransferPayload/Interfaces/TransferPayload'
import { ItemContentsDiffer } from '../../../Utilities/Item/ItemContentsDiffer'
import { ItemInterface } from '../Interfaces/ItemInterface'
import { PayloadSource } from '../../Payload/Types/PayloadSource'
import { ConflictStrategy } from '../Types/ConflictStrategy'
import { PredicateInterface } from '../../../Runtime/Predicate/Interface'
import { SingletonStrategy } from '../Types/SingletonStrategy'
import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface'
import { HistoryEntryInterface } from '../../../Runtime/History/HistoryEntryInterface'
import { isDecryptedItem, isDeletedItem, isEncryptedErroredItem } from '../Interfaces/TypeCheck'
export abstract class GenericItem<P extends PayloadInterface = PayloadInterface> implements ItemInterface<P> {
payload: P
public readonly duplicateOf?: Uuid
public readonly createdAtString?: string
public updatedAtString?: string
public userModifiedDate: Date
constructor(payload: P) {
this.payload = payload
this.duplicateOf = payload.duplicate_of
this.createdAtString = this.created_at && dateToLocalizedString(this.created_at)
this.userModifiedDate = this.serverUpdatedAt || new Date()
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
const timeToAllowSubclassesToFinishConstruction = 0
setTimeout(() => {
deepFreeze(this)
}, timeToAllowSubclassesToFinishConstruction)
}
get uuid() {
return this.payload.uuid
}
get content_type(): ContentType {
return this.payload.content_type
}
get created_at() {
return this.payload.created_at
}
/**
* The date timestamp the server set for this item upon it being synced
* Undefined if never synced to a remote server.
*/
public get serverUpdatedAt(): Date {
return this.payload.serverUpdatedAt
}
public get serverUpdatedAtTimestamp(): number | undefined {
return this.payload.updated_at_timestamp
}
/** @deprecated Use serverUpdatedAt instead */
public get updated_at(): Date | undefined {
return this.serverUpdatedAt
}
get dirty() {
return this.payload.dirty
}
get lastSyncBegan() {
return this.payload.lastSyncBegan
}
get lastSyncEnd() {
return this.payload.lastSyncEnd
}
get duplicate_of() {
return this.payload.duplicate_of
}
public payloadRepresentation(override?: Partial<TransferPayload>): P {
return this.payload.copy(override)
}
/** Whether the item has never been synced to a server */
public get neverSynced(): boolean {
return !this.serverUpdatedAt || this.serverUpdatedAt.getTime() === 0
}
/**
* Subclasses can override this getter to return true if they want only
* one of this item to exist, depending on custom criteria.
*/
public get isSingleton(): boolean {
return false
}
/** The predicate by which singleton items should be unique */
public singletonPredicate<T extends ItemInterface>(): PredicateInterface<T> {
throw 'Must override SNItem.singletonPredicate'
}
public get singletonStrategy(): SingletonStrategy {
return SingletonStrategy.KeepEarliest
}
/**
* Subclasses can override this method and provide their own opinion on whether
* they want to be duplicated. For example, if this.content.x = 12 and
* item.content.x = 13, this function can be overriden to always return
* ConflictStrategy.KeepBase to say 'don't create a duplicate at all, the
* change is not important.'
*
* In the default implementation, we create a duplicate if content differs.
* However, if they only differ by references, we KEEP_LEFT_MERGE_REFS.
*
* Left returns to our current item, and Right refers to the incoming item.
*/
public strategyWhenConflictingWithItem(
item: ItemInterface,
previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
if (isEncryptedErroredItem(this)) {
return ConflictStrategy.KeepBaseDuplicateApply
}
if (this.isSingleton) {
return ConflictStrategy.KeepBase
}
if (isDeletedItem(this)) {
return ConflictStrategy.KeepApply
}
if (isDeletedItem(item)) {
if (this.payload.source === PayloadSource.FileImport) {
return ConflictStrategy.KeepBase
}
return ConflictStrategy.KeepApply
}
if (!isDecryptedItem(item) || !isDecryptedItem(this)) {
return ConflictStrategy.KeepBaseDuplicateApply
}
const contentDiffers = ItemContentsDiffer(this, item)
if (!contentDiffers) {
return ConflictStrategy.KeepApply
}
const itemsAreDifferentExcludingRefs = ItemContentsDiffer(this, item, ['references'])
if (itemsAreDifferentExcludingRefs) {
if (previousRevision) {
/**
* If previousRevision.content === incomingValue.content, this means the
* change that was rejected by the server is in fact a legitimate change,
* because the value the client had previously matched with the server's,
* and this new change is being built on top of that state, and should therefore
* be chosen as the winner, with no need for a conflict.
*/
if (!ItemContentsDiffer(previousRevision.itemFromPayload(), item)) {
return ConflictStrategy.KeepBase
}
}
const twentySeconds = 20_000
if (
/**
* If the incoming item comes from an import, treat it as
* less important than the existing one.
*/
item.payload.source === PayloadSource.FileImport ||
/**
* If the user is actively editing our item, duplicate the incoming item
* to avoid creating surprises in the client's UI.
*/
Date.now() - this.userModifiedDate.getTime() < twentySeconds
) {
return ConflictStrategy.KeepBaseDuplicateApply
} else {
return ConflictStrategy.DuplicateBaseKeepApply
}
} else {
/** Only the references have changed; merge them. */
return ConflictStrategy.KeepBaseMergeRefs
}
}
public satisfiesPredicate(predicate: PredicateInterface<ItemInterface>): boolean {
return predicate.matchesItem(this)
}
}

View File

@@ -0,0 +1,45 @@
import { Uuid } from '@standardnotes/common'
import { AppDataField } from '../Types/AppDataField'
import { ComponentDataDomain, DefaultAppDomain } from '../Types/DefaultAppDomain'
import { ContentReference } from '../../Reference/ContentReference'
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
import { ItemContent } from '../../Content/ItemContent'
import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload'
import { ItemInterface } from './ItemInterface'
import { SortableItem } from '../../../Runtime/Collection/CollectionSort'
import { DecryptedTransferPayload } from '../../TransferPayload/Interfaces/DecryptedTransferPayload'
import { SearchableItem } from '../../../Runtime/Display'
export interface DecryptedItemInterface<C extends ItemContent = ItemContent>
extends ItemInterface<DecryptedPayloadInterface<C>>,
SortableItem,
SearchableItem {
readonly content: C
readonly conflictOf?: Uuid
readonly duplicateOf?: Uuid
readonly protected: boolean
readonly trashed: boolean
readonly pinned: boolean
readonly archived: boolean
readonly locked: boolean
readonly userModifiedDate: Date
readonly references: ContentReference[]
getAppDomainValueWithDefault<T, D extends T>(key: AppDataField | PrefKey, defaultValue: D): T
getAppDomainValue<T>(key: AppDataField | PrefKey): T | undefined
isItemContentEqualWith(otherItem: DecryptedItemInterface): boolean
payloadRepresentation(override?: Partial<DecryptedTransferPayload<C>>): DecryptedPayloadInterface<C>
isReferencingItem(item: DecryptedItemInterface): boolean
getDomainData(domain: typeof ComponentDataDomain | typeof DefaultAppDomain): undefined | Record<string, unknown>
contentKeysToIgnoreWhenCheckingEquality<C extends ItemContent = ItemContent>(): (keyof C)[]
appDataContentKeysToIgnoreWhenCheckingEquality(): AppDataField[]
getContentCopy(): C
}

View File

@@ -0,0 +1,7 @@
import { DeletedPayloadInterface } from './../../Payload/Interfaces/DeletedPayload'
import { ItemInterface } from './ItemInterface'
export interface DeletedItemInterface extends ItemInterface<DeletedPayloadInterface> {
readonly deleted: true
readonly content: undefined
}

View File

@@ -0,0 +1,11 @@
import { ProtocolVersion } from '@standardnotes/common'
import { EncryptedPayloadInterface } from '../../Payload/Interfaces/EncryptedPayload'
import { ItemInterface } from './ItemInterface'
export interface EncryptedItemInterface extends ItemInterface<EncryptedPayloadInterface> {
content: string
version: ProtocolVersion
errorDecrypting: boolean
waitingForKey?: boolean
auth_hash?: string
}

View File

@@ -0,0 +1,41 @@
import { Uuid, ContentType } from '@standardnotes/common'
import { TransferPayload } from './../../TransferPayload/Interfaces/TransferPayload'
import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface'
import { PredicateInterface } from '../../../Runtime/Predicate/Interface'
import { HistoryEntryInterface } from '../../../Runtime/History'
import { ConflictStrategy } from '../Types/ConflictStrategy'
import { SingletonStrategy } from '../Types/SingletonStrategy'
export interface ItemInterface<P extends PayloadInterface = PayloadInterface> {
payload: P
readonly conflictOf?: Uuid
readonly duplicateOf?: Uuid
readonly createdAtString?: string
readonly updatedAtString?: string
uuid: Uuid
content_type: ContentType
created_at: Date
serverUpdatedAt: Date
serverUpdatedAtTimestamp: number | undefined
dirty: boolean | undefined
lastSyncBegan: Date | undefined
lastSyncEnd: Date | undefined
neverSynced: boolean
duplicate_of: string | undefined
isSingleton: boolean
updated_at: Date | undefined
singletonPredicate<T extends ItemInterface>(): PredicateInterface<T>
singletonStrategy: SingletonStrategy
strategyWhenConflictingWithItem(item: ItemInterface, previousRevision?: HistoryEntryInterface): ConflictStrategy
satisfiesPredicate(predicate: PredicateInterface<ItemInterface>): boolean
payloadRepresentation(override?: Partial<TransferPayload>): P
}

View File

@@ -0,0 +1,31 @@
import { EncryptedItemInterface } from './EncryptedItem'
import { DeletedItemInterface } from './DeletedItem'
import { ItemInterface } from './ItemInterface'
import { DecryptedItemInterface } from './DecryptedItem'
import { isDecryptedPayload, isDeletedPayload, isEncryptedPayload } from '../../Payload/Interfaces/TypeCheck'
export function isDecryptedItem(item: ItemInterface): item is DecryptedItemInterface {
return isDecryptedPayload(item.payload)
}
export function isEncryptedItem(item: ItemInterface): item is EncryptedItemInterface {
return isEncryptedPayload(item.payload)
}
export function isNotEncryptedItem(
item: DecryptedItemInterface | DeletedItemInterface | EncryptedItemInterface,
): item is DecryptedItemInterface | DeletedItemInterface {
return !isEncryptedItem(item)
}
export function isDeletedItem(item: ItemInterface): item is DeletedItemInterface {
return isDeletedPayload(item.payload)
}
export function isDecryptedOrDeletedItem(item: ItemInterface): item is DecryptedItemInterface | DeletedItemInterface {
return isDecryptedItem(item) || isDeletedItem(item)
}
export function isEncryptedErroredItem(item: ItemInterface): boolean {
return isEncryptedItem(item) && item.errorDecrypting === true
}

View File

@@ -0,0 +1,9 @@
import { ItemContent } from '../../Content/ItemContent'
import { DecryptedItemInterface } from './DecryptedItem'
import { DeletedItemInterface } from './DeletedItem'
import { EncryptedItemInterface } from './EncryptedItem'
export type AnyItemInterface<C extends ItemContent = ItemContent> =
| EncryptedItemInterface
| DecryptedItemInterface<C>
| DeletedItemInterface

View File

@@ -0,0 +1,145 @@
import { DecryptedItemInterface } from './../Interfaces/DecryptedItem'
import { Copy } from '@standardnotes/utils'
import { MutationType } from '../Types/MutationType'
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
import { Uuid } from '@standardnotes/common'
import { ItemContent } from '../../Content/ItemContent'
import { AppDataField } from '../Types/AppDataField'
import { DefaultAppDomain, DomainDataValueType, ItemDomainKey } from '../Types/DefaultAppDomain'
import { ItemMutator } from './ItemMutator'
import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPayload'
import { ItemInterface } from '../Interfaces/ItemInterface'
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
export class DecryptedItemMutator<C extends ItemContent = ItemContent> extends ItemMutator<
DecryptedPayloadInterface<C>,
DecryptedItemInterface<C>
> {
protected mutableContent: C
constructor(item: DecryptedItemInterface<C>, type: MutationType) {
super(item, type)
const mutableCopy = Copy(this.immutablePayload.content)
this.mutableContent = mutableCopy
}
public override getResult() {
if (this.type === MutationType.NonDirtying) {
return this.immutablePayload.copy({
content: this.mutableContent,
})
}
if (this.type === MutationType.UpdateUserTimestamps) {
this.userModifiedDate = new Date()
} else {
const currentValue = this.immutableItem.userModifiedDate
if (!currentValue) {
this.userModifiedDate = new Date(this.immutableItem.serverUpdatedAt)
}
}
const result = this.immutablePayload.copy({
content: this.mutableContent,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
})
return result
}
public override setBeginSync(began: Date, globalDirtyIndex: number) {
this.immutablePayload = this.immutablePayload.copy({
content: this.mutableContent,
lastSyncBegan: began,
globalDirtyIndexAtLastSync: globalDirtyIndex,
})
}
/** Not recommended to use as this might break item schema if used incorrectly */
public setCustomContent(content: C): void {
this.mutableContent = Copy(content)
}
public set userModifiedDate(date: Date) {
this.setAppDataItem(AppDataField.UserModifiedDate, date)
}
public set conflictOf(conflictOf: Uuid | undefined) {
this.mutableContent.conflict_of = conflictOf
}
public set protected(isProtected: boolean) {
this.mutableContent.protected = isProtected
}
public set trashed(trashed: boolean) {
this.mutableContent.trashed = trashed
}
public set pinned(pinned: boolean) {
this.setAppDataItem(AppDataField.Pinned, pinned)
}
public set archived(archived: boolean) {
this.setAppDataItem(AppDataField.Archived, archived)
}
public set locked(locked: boolean) {
this.setAppDataItem(AppDataField.Locked, locked)
}
/**
* Overwrites the entirety of this domain's data with the data arg.
*/
public setDomainData(data: DomainDataValueType, domain: ItemDomainKey): void {
if (!this.mutableContent.appData) {
this.mutableContent.appData = {
[DefaultAppDomain]: {},
}
}
this.mutableContent.appData[domain] = data
}
/**
* First gets the domain data for the input domain.
* Then sets data[key] = value
*/
public setDomainDataKey(key: keyof DomainDataValueType, value: unknown, domain: ItemDomainKey): void {
if (!this.mutableContent.appData) {
this.mutableContent.appData = {
[DefaultAppDomain]: {},
}
}
if (!this.mutableContent.appData[domain]) {
this.mutableContent.appData[domain] = {}
}
const domainData = this.mutableContent.appData[domain] as DomainDataValueType
domainData[key] = value
}
public setAppDataItem(key: AppDataField | PrefKey, value: unknown) {
this.setDomainDataKey(key, value, DefaultAppDomain)
}
public e2ePendingRefactor_addItemAsRelationship(item: DecryptedItemInterface) {
const references = this.mutableContent.references || []
if (!references.find((r) => r.uuid === item.uuid)) {
references.push({
uuid: item.uuid,
content_type: item.content_type,
})
}
this.mutableContent.references = references
}
public removeItemAsRelationship(item: ItemInterface) {
let references = this.mutableContent.references || []
references = references.filter((r) => r.uuid !== item.uuid)
this.mutableContent.references = references
}
}

View File

@@ -0,0 +1,30 @@
import { DeletedPayload } from './../../Payload/Implementations/DeletedPayload'
import { DeletedPayloadInterface, PayloadInterface } from '../../Payload'
import { ItemInterface } from '../Interfaces/ItemInterface'
import { ItemMutator } from './ItemMutator'
import { MutationType } from '../Types/MutationType'
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
export class DeleteItemMutator<
I extends ItemInterface<PayloadInterface> = ItemInterface<PayloadInterface>,
> extends ItemMutator<PayloadInterface, I> {
public getDeletedResult(): DeletedPayloadInterface {
const dirtying = this.type !== MutationType.NonDirtying
const result = new DeletedPayload(
{
...this.immutablePayload.ejected(),
deleted: true,
content: undefined,
dirty: dirtying ? true : this.immutablePayload.dirty,
dirtyIndex: dirtying ? getIncrementedDirtyIndex() : this.immutablePayload.dirtyIndex,
},
this.immutablePayload.source,
)
return result
}
public override getResult(): PayloadInterface {
throw Error('Must use getDeletedResult')
}
}

View File

@@ -0,0 +1,65 @@
import { MutationType } from '../Types/MutationType'
import { PayloadInterface } from '../../Payload'
import { ItemInterface } from '../Interfaces/ItemInterface'
import { TransferPayload } from '../../TransferPayload'
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
/**
* An item mutator takes in an item, and an operation, and returns the resulting payload.
* Subclasses of mutators can modify the content field directly, but cannot modify the payload directly.
* All changes to the payload must occur by copying the payload and reassigning its value.
*/
export class ItemMutator<
P extends PayloadInterface<TransferPayload> = PayloadInterface<TransferPayload>,
I extends ItemInterface<P> = ItemInterface<P>,
> {
public readonly immutableItem: I
protected immutablePayload: P
protected readonly type: MutationType
constructor(item: I, type: MutationType) {
this.immutableItem = item
this.type = type
this.immutablePayload = item.payload
}
public getUuid() {
return this.immutablePayload.uuid
}
public getItem(): I {
return this.immutableItem
}
public getResult(): P {
if (this.type === MutationType.NonDirtying) {
return this.immutablePayload.copy()
}
const result = this.immutablePayload.copy({
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
})
return result
}
public setBeginSync(began: Date, globalDirtyIndex: number) {
this.immutablePayload = this.immutablePayload.copy({
lastSyncBegan: began,
globalDirtyIndexAtLastSync: globalDirtyIndex,
})
}
public set errorDecrypting(_: boolean) {
throw Error('This method is no longer implemented')
}
public set updated_at(_: Date) {
throw Error('This method is no longer implemented')
}
public set updated_at_timestamp(_: number) {
throw Error('This method is no longer implemented')
}
}

View File

@@ -0,0 +1,13 @@
export enum AppDataField {
Pinned = 'pinned',
Archived = 'archived',
Locked = 'locked',
UserModifiedDate = 'client_updated_at',
DefaultEditor = 'defaultEditor',
MobileRules = 'mobileRules',
NotAvailableOnMobile = 'notAvailableOnMobile',
MobileActive = 'mobileActive',
LastSize = 'lastSize',
PrefersPlainEditor = 'prefersPlainEditor',
ComponentInstallError = 'installError',
}

View File

@@ -0,0 +1,7 @@
export enum ConflictStrategy {
KeepBase = 1,
KeepApply = 2,
KeepBaseDuplicateApply = 3,
DuplicateBaseKeepApply = 4,
KeepBaseMergeRefs = 5,
}

View File

@@ -0,0 +1,17 @@
import { PrefKey } from '../../../Syncable/UserPrefs/PrefKey'
import { AppDataField } from './AppDataField'
export const DefaultAppDomain = 'org.standardnotes.sn'
/* This domain will be used to save context item client data */
export const ComponentDataDomain = 'org.standardnotes.sn.components'
export type ItemDomainKey = typeof DefaultAppDomain | typeof ComponentDataDomain
export type AppDomainValueType = Partial<Record<AppDataField | PrefKey, unknown>>
export type ComponentDomainValueType = Record<string, unknown>
export type DomainDataValueType = AppDomainValueType | ComponentDomainValueType
export type AppData = {
[DefaultAppDomain]: AppDomainValueType
[ComponentDataDomain]?: ComponentDomainValueType
}

View File

@@ -0,0 +1,14 @@
export enum MutationType {
UpdateUserTimestamps = 1,
/**
* The item was changed as part of an internal operation, such as a migration, or, a user
* interaction that shouldn't modify timestamps (pinning, protecting, etc).
*/
NoUpdateUserTimestamps = 2,
/**
* The item was changed as part of an internal function that wishes to modify
* internal item properties, such as lastSyncBegan, without modifying the item's dirty
* state. By default all other mutation types will result in a dirtied result.
*/
NonDirtying = 3,
}

View File

@@ -0,0 +1,3 @@
export enum SingletonStrategy {
KeepEarliest = 1,
}

View File

@@ -0,0 +1,29 @@
export * from '../Reference/AnonymousReference'
export * from '../Reference/ContenteReferenceType'
export * from '../Reference/ContentReference'
export * from '../Reference/FileToNoteReference'
export * from '../Reference/Functions'
export * from '../Reference/LegacyAnonymousReference'
export * from '../Reference/LegacyTagToNoteReference'
export * from '../Reference/Reference'
export * from '../Reference/TagToParentTagReference'
export * from './Implementations/DecryptedItem'
export * from './Implementations/DecryptedItem'
export * from './Implementations/DeletedItem'
export * from './Implementations/EncryptedItem'
export * from './Implementations/GenericItem'
export * from './Interfaces/DecryptedItem'
export * from './Interfaces/DeletedItem'
export * from './Interfaces/EncryptedItem'
export * from './Interfaces/ItemInterface'
export * from './Interfaces/TypeCheck'
export * from './Mutator/DecryptedItemMutator'
export * from './Mutator/DeleteMutator'
export * from './Mutator/ItemMutator'
export * from './Types/AppDataField'
export * from './Types/AppDataField'
export * from './Types/ConflictStrategy'
export * from './Types/DefaultAppDomain'
export * from './Types/DefaultAppDomain'
export * from './Types/MutationType'
export * from './Types/SingletonStrategy'

View File

@@ -0,0 +1,71 @@
import { Uuid } from '@standardnotes/common'
import { Copy } from '@standardnotes/utils'
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
import { FillItemContent, ItemContent } from '../../Content/ItemContent'
import { ContentReference } from '../../Reference/ContentReference'
import { DecryptedTransferPayload } from '../../TransferPayload/Interfaces/DecryptedTransferPayload'
import { DecryptedPayloadInterface } from '../Interfaces/DecryptedPayload'
import { PayloadSource } from '../Types/PayloadSource'
import { PurePayload } from './PurePayload'
export class DecryptedPayload<
C extends ItemContent = ItemContent,
T extends DecryptedTransferPayload<C> = DecryptedTransferPayload<C>,
>
extends PurePayload<T>
implements DecryptedPayloadInterface<C>
{
override readonly content: C
override readonly deleted: false
constructor(rawPayload: T, source = PayloadSource.Constructor) {
super(rawPayload, source)
this.content = Copy(FillItemContent<C>(rawPayload.content))
this.deleted = false
}
get references(): ContentReference[] {
return this.content.references || []
}
public getReference(uuid: Uuid): ContentReference {
const result = this.references.find((ref) => ref.uuid === uuid)
if (!result) {
throw new Error('Reference not found')
}
return result
}
override ejected(): DecryptedTransferPayload<C> {
return {
...super.ejected(),
content: this.content,
deleted: this.deleted,
}
}
copy(override?: Partial<T>, source = this.source): this {
const result = new DecryptedPayload(
{
...this.ejected(),
...override,
},
source,
)
return result as this
}
copyAsSyncResolved(override?: Partial<T> & SyncResolvedParams, source = this.source): SyncResolvedPayload {
const result = new DecryptedPayload(
{
...this.ejected(),
...override,
},
source,
)
return result as SyncResolvedPayload
}
}

View File

@@ -0,0 +1,54 @@
import { DeletedTransferPayload } from './../../TransferPayload/Interfaces/DeletedTransferPayload'
import { DeletedPayloadInterface } from '../Interfaces/DeletedPayload'
import { PayloadSource } from '../Types/PayloadSource'
import { PurePayload } from './PurePayload'
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
export class DeletedPayload extends PurePayload<DeletedTransferPayload> implements DeletedPayloadInterface {
override readonly deleted: true
override readonly content: undefined
constructor(rawPayload: DeletedTransferPayload, source = PayloadSource.Constructor) {
super(rawPayload, source)
this.deleted = true
this.content = undefined
}
get discardable(): boolean | undefined {
return !this.dirty
}
override ejected(): DeletedTransferPayload {
return {
...super.ejected(),
deleted: this.deleted,
content: undefined,
}
}
copy(override?: Partial<DeletedTransferPayload>, source = this.source): this {
const result = new DeletedPayload(
{
...this.ejected(),
...override,
},
source,
)
return result as this
}
copyAsSyncResolved(
override?: Partial<DeletedTransferPayload> & SyncResolvedParams,
source = this.source,
): SyncResolvedPayload {
const result = new DeletedPayload(
{
...this.ejected(),
...override,
},
source,
)
return result as SyncResolvedPayload
}
}

View File

@@ -0,0 +1,68 @@
import { ProtocolVersion, protocolVersionFromEncryptedString } from '@standardnotes/common'
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
import { EncryptedTransferPayload } from '../../TransferPayload/Interfaces/EncryptedTransferPayload'
import { EncryptedPayloadInterface } from '../Interfaces/EncryptedPayload'
import { PayloadSource } from '../Types/PayloadSource'
import { PurePayload } from './PurePayload'
export class EncryptedPayload extends PurePayload<EncryptedTransferPayload> implements EncryptedPayloadInterface {
override readonly content: string
override readonly deleted: false
readonly auth_hash?: string
readonly enc_item_key: string
readonly errorDecrypting: boolean
readonly items_key_id: string | undefined
readonly version: ProtocolVersion
readonly waitingForKey: boolean
constructor(rawPayload: EncryptedTransferPayload, source = PayloadSource.Constructor) {
super(rawPayload, source)
this.auth_hash = rawPayload.auth_hash
this.content = rawPayload.content
this.deleted = false
this.enc_item_key = rawPayload.enc_item_key
this.errorDecrypting = rawPayload.errorDecrypting
this.items_key_id = rawPayload.items_key_id
this.version = protocolVersionFromEncryptedString(this.content)
this.waitingForKey = rawPayload.waitingForKey
}
override ejected(): EncryptedTransferPayload {
return {
...super.ejected(),
enc_item_key: this.enc_item_key,
items_key_id: this.items_key_id,
auth_hash: this.auth_hash,
errorDecrypting: this.errorDecrypting,
waitingForKey: this.waitingForKey,
content: this.content,
deleted: this.deleted,
}
}
copy(override?: Partial<EncryptedTransferPayload>, source = this.source): this {
const result = new EncryptedPayload(
{
...this.ejected(),
...override,
},
source,
)
return result as this
}
copyAsSyncResolved(
override?: Partial<EncryptedTransferPayload> & SyncResolvedParams,
source = this.source,
): SyncResolvedPayload {
const result = new EncryptedPayload(
{
...this.ejected(),
...override,
},
source,
)
return result as SyncResolvedPayload
}
}

View File

@@ -0,0 +1,104 @@
import { ContentType } from '@standardnotes/common'
import { deepFreeze, useBoolean } from '@standardnotes/utils'
import { PayloadInterface } from '../Interfaces/PayloadInterface'
import { PayloadSource } from '../Types/PayloadSource'
import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload'
import { ItemContent } from '../../Content/ItemContent'
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
type RequiredKeepUndefined<T> = { [K in keyof T]-?: [T[K]] } extends infer U
? U extends Record<keyof U, [unknown]>
? { [K in keyof U]: U[K][0] }
: never
: never
export abstract class PurePayload<T extends TransferPayload<C>, C extends ItemContent = ItemContent>
implements PayloadInterface<T>
{
readonly source: PayloadSource
readonly uuid: string
readonly content_type: ContentType
readonly deleted: boolean
readonly content: C | string | undefined
readonly created_at: Date
readonly updated_at: Date
readonly created_at_timestamp: number
readonly updated_at_timestamp: number
readonly dirtyIndex?: number
readonly globalDirtyIndexAtLastSync?: number
readonly dirty?: boolean
readonly lastSyncBegan?: Date
readonly lastSyncEnd?: Date
readonly duplicate_of?: string
constructor(rawPayload: T, source = PayloadSource.Constructor) {
this.source = source
this.uuid = rawPayload.uuid
if (!this.uuid) {
throw Error(
`Attempting to construct payload with null uuid
Content type: ${rawPayload.content_type}`,
)
}
this.content = rawPayload.content
this.content_type = rawPayload.content_type
this.deleted = useBoolean(rawPayload.deleted, false)
this.dirty = rawPayload.dirty
this.duplicate_of = rawPayload.duplicate_of
this.created_at = new Date(rawPayload.created_at || new Date())
this.updated_at = new Date(rawPayload.updated_at || 0)
this.created_at_timestamp = rawPayload.created_at_timestamp || 0
this.updated_at_timestamp = rawPayload.updated_at_timestamp || 0
this.lastSyncBegan = rawPayload.lastSyncBegan ? new Date(rawPayload.lastSyncBegan) : undefined
this.lastSyncEnd = rawPayload.lastSyncEnd ? new Date(rawPayload.lastSyncEnd) : undefined
this.dirtyIndex = rawPayload.dirtyIndex
this.globalDirtyIndexAtLastSync = rawPayload.globalDirtyIndexAtLastSync
const timeToAllowSubclassesToFinishConstruction = 0
setTimeout(() => {
deepFreeze(this)
}, timeToAllowSubclassesToFinishConstruction)
}
ejected(): TransferPayload {
const comprehensive: RequiredKeepUndefined<TransferPayload> = {
uuid: this.uuid,
content: this.content,
deleted: this.deleted,
content_type: this.content_type,
created_at: this.created_at,
updated_at: this.updated_at,
created_at_timestamp: this.created_at_timestamp,
updated_at_timestamp: this.updated_at_timestamp,
dirty: this.dirty,
duplicate_of: this.duplicate_of,
dirtyIndex: this.dirtyIndex,
globalDirtyIndexAtLastSync: this.globalDirtyIndexAtLastSync,
lastSyncBegan: this.lastSyncBegan,
lastSyncEnd: this.lastSyncEnd,
}
return comprehensive
}
public get serverUpdatedAt(): Date {
return this.updated_at
}
public get serverUpdatedAtTimestamp(): number {
return this.updated_at_timestamp
}
abstract copy(override?: Partial<TransferPayload>, source?: PayloadSource): this
abstract copyAsSyncResolved(override?: Partial<T> & SyncResolvedParams, source?: PayloadSource): SyncResolvedPayload
}

View File

@@ -0,0 +1,15 @@
import { Uuid } from '@standardnotes/common'
import { DecryptedTransferPayload } from './../../TransferPayload/Interfaces/DecryptedTransferPayload'
import { ItemContent } from '../../Content/ItemContent'
import { ContentReference } from '../../Reference/ContentReference'
import { PayloadInterface } from './PayloadInterface'
export interface DecryptedPayloadInterface<C extends ItemContent = ItemContent>
extends PayloadInterface<DecryptedTransferPayload> {
readonly content: C
deleted: false
ejected(): DecryptedTransferPayload<C>
get references(): ContentReference[]
getReference(uuid: Uuid): ContentReference
}

View File

@@ -0,0 +1,15 @@
import { DeletedTransferPayload } from '../../TransferPayload'
import { PayloadInterface } from './PayloadInterface'
export interface DeletedPayloadInterface extends PayloadInterface<DeletedTransferPayload> {
readonly deleted: true
readonly content: undefined
/**
* Whether a payload can be discarded and removed from storage.
* This value is true if a payload is marked as deleted and not dirty.
*/
discardable: boolean | undefined
ejected(): DeletedTransferPayload
}

View File

@@ -0,0 +1,18 @@
import { ProtocolVersion } from '@standardnotes/common'
import { EncryptedTransferPayload } from '../../TransferPayload/Interfaces/EncryptedTransferPayload'
import { PayloadInterface } from './PayloadInterface'
export interface EncryptedPayloadInterface extends PayloadInterface<EncryptedTransferPayload> {
readonly content: string
readonly deleted: false
readonly enc_item_key: string
readonly items_key_id: string | undefined
readonly errorDecrypting: boolean
readonly waitingForKey: boolean
readonly version: ProtocolVersion
/** @deprecated */
readonly auth_hash?: string
ejected(): EncryptedTransferPayload
}

View File

@@ -0,0 +1,41 @@
import { SyncResolvedParams, SyncResolvedPayload } from './../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
import { ContentType, Uuid } from '@standardnotes/common'
import { ItemContent } from '../../Content/ItemContent'
import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload'
import { PayloadSource } from '../Types/PayloadSource'
export interface PayloadInterface<T extends TransferPayload = TransferPayload, C extends ItemContent = ItemContent> {
readonly source: PayloadSource
readonly uuid: Uuid
readonly content_type: ContentType
content: C | string | undefined
deleted: boolean
/** updated_at is set by the server only, and not the client.*/
readonly updated_at: Date
readonly created_at: Date
readonly created_at_timestamp: number
readonly updated_at_timestamp: number
get serverUpdatedAt(): Date
get serverUpdatedAtTimestamp(): number
readonly dirtyIndex?: number
readonly globalDirtyIndexAtLastSync?: number
readonly dirty?: boolean
readonly lastSyncBegan?: Date
readonly lastSyncEnd?: Date
readonly duplicate_of?: Uuid
/**
* "Ejected" means a payload for
* generic, non-contextual consumption, such as saving to a backup file or syncing
* with a server.
*/
ejected(): TransferPayload
copy(override?: Partial<T>, source?: PayloadSource): this
copyAsSyncResolved(override?: Partial<T> & SyncResolvedParams, source?: PayloadSource): SyncResolvedPayload
}

View File

@@ -0,0 +1,29 @@
import { ItemContent } from '../../Content/ItemContent'
import {
isDecryptedTransferPayload,
isDeletedTransferPayload,
isEncryptedTransferPayload,
isErrorDecryptingTransferPayload,
} from '../../TransferPayload'
import { DecryptedPayloadInterface } from './DecryptedPayload'
import { DeletedPayloadInterface } from './DeletedPayload'
import { EncryptedPayloadInterface } from './EncryptedPayload'
import { PayloadInterface } from './PayloadInterface'
export function isDecryptedPayload<C extends ItemContent = ItemContent>(
payload: PayloadInterface,
): payload is DecryptedPayloadInterface<C> {
return isDecryptedTransferPayload(payload)
}
export function isEncryptedPayload(payload: PayloadInterface): payload is EncryptedPayloadInterface {
return isEncryptedTransferPayload(payload)
}
export function isDeletedPayload(payload: PayloadInterface): payload is DeletedPayloadInterface {
return isDeletedTransferPayload(payload)
}
export function isErrorDecryptingPayload(payload: PayloadInterface): payload is EncryptedPayloadInterface {
return isErrorDecryptingTransferPayload(payload)
}

View File

@@ -0,0 +1,11 @@
import { ItemContent } from '../../Content/ItemContent'
import { DecryptedPayloadInterface } from './DecryptedPayload'
import { DeletedPayloadInterface } from './DeletedPayload'
import { EncryptedPayloadInterface } from './EncryptedPayload'
export type FullyFormedPayloadInterface<C extends ItemContent = ItemContent> =
| DecryptedPayloadInterface<C>
| EncryptedPayloadInterface
| DeletedPayloadInterface
export type AnyNonDecryptedPayloadInterface = EncryptedPayloadInterface | DeletedPayloadInterface

View File

@@ -0,0 +1,43 @@
export enum PayloadEmitSource {
/** When an observer registers to stream items, the items are pushed immediately to the observer */
InitialObserverRegistrationPush = 1,
/**
* Payload when a client modifies item property then maps it to update UI.
* This also indicates that the item was dirtied
*/
LocalChanged,
LocalInserted,
LocalDatabaseLoaded,
/** The payload returned by offline sync operation */
OfflineSyncSaved,
LocalRetrieved,
FileImport,
ComponentRetrieved,
/** Payloads received from an external component with the intention of creating a new item */
ComponentCreated,
/**
* When the payloads are about to sync, they are emitted by the sync service with updated
* values of lastSyncBegan. Payloads emitted from this source indicate that these payloads
* have been saved to disk, and are about to be synced
*/
PreSyncSave,
RemoteRetrieved,
RemoteSaved,
}
/**
* Whether the changed payload represents only an internal change that shouldn't
* require a UI refresh
*/
export function isPayloadSourceInternalChange(source: PayloadEmitSource): boolean {
return [PayloadEmitSource.RemoteSaved, PayloadEmitSource.PreSyncSave].includes(source)
}
export function isPayloadSourceRetrieved(source: PayloadEmitSource): boolean {
return [PayloadEmitSource.RemoteRetrieved, PayloadEmitSource.ComponentRetrieved].includes(source)
}

View File

@@ -0,0 +1,13 @@
export enum PayloadSource {
/**
* Payloads with a source of Constructor means that the payload was created
* in isolated space by the caller, and does not yet have any app-related affiliation.
*/
Constructor = 1,
RemoteRetrieved,
RemoteSaved,
FileImport,
}

View File

@@ -0,0 +1,8 @@
export function PayloadTimestampDefaults() {
return {
updated_at: new Date(0),
created_at: new Date(),
updated_at_timestamp: 0,
created_at_timestamp: 0,
}
}

View File

@@ -0,0 +1,13 @@
export * from './Implementations/PurePayload'
export * from './Implementations/DecryptedPayload'
export * from './Implementations/EncryptedPayload'
export * from './Implementations/DeletedPayload'
export * from './Interfaces/DecryptedPayload'
export * from './Interfaces/DeletedPayload'
export * from './Interfaces/EncryptedPayload'
export * from './Interfaces/PayloadInterface'
export * from './Interfaces/TypeCheck'
export * from './Interfaces/UnionTypes'
export * from './Types/PayloadSource'
export * from './Types/EmitSource'
export * from './Types/TimestampDefaults'

View File

@@ -0,0 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { ContenteReferenceType } from './ContenteReferenceType'
export interface AnonymousReference {
uuid: string
content_type: ContentType
reference_type: ContenteReferenceType
}

View File

@@ -0,0 +1,4 @@
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
import { Reference } from './Reference'
export type ContentReference = LegacyAnonymousReference | Reference

View File

@@ -0,0 +1,5 @@
export enum ContenteReferenceType {
TagToParentTag = 'TagToParentTag',
FileToNote = 'FileToNote',
TagToFile = 'TagToFile',
}

View File

@@ -0,0 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContenteReferenceType } from './ContenteReferenceType'
export interface FileToNoteReference extends AnonymousReference {
content_type: ContentType.Note
reference_type: ContenteReferenceType.FileToNote
}

View File

@@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ContentType } from '@standardnotes/common'
import { ItemInterface } from '../Item/Interfaces/ItemInterface'
import { ContenteReferenceType } from './ContenteReferenceType'
import { ContentReference } from './ContentReference'
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
import { LegacyTagToNoteReference } from './LegacyTagToNoteReference'
import { Reference } from './Reference'
import { TagToParentTagReference } from './TagToParentTagReference'
export const isLegacyAnonymousReference = (x: ContentReference): x is LegacyAnonymousReference => {
return (x as any).reference_type === undefined
}
export const isReference = (x: ContentReference): x is Reference => {
return (x as any).reference_type !== undefined
}
export const isLegacyTagToNoteReference = (
x: LegacyAnonymousReference,
currentItem: ItemInterface,
): x is LegacyTagToNoteReference => {
const isReferenceToANote = x.content_type === ContentType.Note
const isReferenceFromATag = currentItem.content_type === ContentType.Tag
return isReferenceToANote && isReferenceFromATag
}
export const isTagToParentTagReference = (x: ContentReference): x is TagToParentTagReference => {
return isReference(x) && x.reference_type === ContenteReferenceType.TagToParentTag
}

View File

@@ -0,0 +1,4 @@
export interface LegacyAnonymousReference {
uuid: string
content_type: string
}

View File

@@ -0,0 +1,6 @@
import { ContentType } from '@standardnotes/common'
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
export interface LegacyTagToNoteReference extends LegacyAnonymousReference {
content_type: ContentType.Note
}

View File

@@ -0,0 +1,3 @@
import { TagToParentTagReference } from './TagToParentTagReference'
export type Reference = TagToParentTagReference

View File

@@ -0,0 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContenteReferenceType } from './ContenteReferenceType'
export interface TagToFileReference extends AnonymousReference {
content_type: ContentType.File
reference_type: ContenteReferenceType.TagToFile
}

View File

@@ -0,0 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContenteReferenceType } from './ContenteReferenceType'
export interface TagToParentTagReference extends AnonymousReference {
content_type: ContentType.Tag
reference_type: ContenteReferenceType.TagToParentTag
}

View File

@@ -0,0 +1,6 @@
import { ItemContent } from '../../Content/ItemContent'
import { TransferPayload } from './TransferPayload'
export interface DecryptedTransferPayload<C extends ItemContent = ItemContent> extends TransferPayload {
content: C
}

View File

@@ -0,0 +1,6 @@
import { TransferPayload } from './TransferPayload'
export interface DeletedTransferPayload extends TransferPayload {
content: undefined
deleted: true
}

View File

@@ -0,0 +1,11 @@
import { TransferPayload } from './TransferPayload'
export interface EncryptedTransferPayload extends TransferPayload {
content: string
enc_item_key: string
items_key_id: string | undefined
errorDecrypting: boolean
waitingForKey: boolean
/** @deprecated */
auth_hash?: string
}

View File

@@ -0,0 +1,23 @@
import { ContentType, Uuid } from '@standardnotes/common'
import { ItemContent } from '../../Content/ItemContent'
export interface TransferPayload<C extends ItemContent = ItemContent> {
uuid: Uuid
content_type: ContentType
content: C | string | undefined
deleted?: boolean
updated_at: Date
created_at: Date
created_at_timestamp: number
updated_at_timestamp: number
dirtyIndex?: number
globalDirtyIndexAtLastSync?: number
dirty?: boolean
lastSyncBegan?: Date
lastSyncEnd?: Date
duplicate_of?: Uuid
}

View File

@@ -0,0 +1,28 @@
import { isObject, isString } from '@standardnotes/utils'
import { DecryptedTransferPayload } from './DecryptedTransferPayload'
import { DeletedTransferPayload } from './DeletedTransferPayload'
import { EncryptedTransferPayload } from './EncryptedTransferPayload'
import { TransferPayload } from './TransferPayload'
export type FullyFormedTransferPayload = DecryptedTransferPayload | EncryptedTransferPayload | DeletedTransferPayload
export function isDecryptedTransferPayload(payload: TransferPayload): payload is DecryptedTransferPayload {
return isObject(payload.content)
}
export function isEncryptedTransferPayload(payload: TransferPayload): payload is EncryptedTransferPayload {
return 'content' in payload && isString(payload.content)
}
export function isErrorDecryptingTransferPayload(payload: TransferPayload): payload is EncryptedTransferPayload {
return isEncryptedTransferPayload(payload) && payload.errorDecrypting === true
}
export function isDeletedTransferPayload(payload: TransferPayload): payload is DeletedTransferPayload {
return 'deleted' in payload && payload.deleted === true
}
export function isCorruptTransferPayload(payload: TransferPayload): boolean {
const invalidDeletedState = payload.deleted === true && payload.content != undefined
return payload.uuid == undefined || invalidDeletedState
}

View File

@@ -0,0 +1,5 @@
export * from './Interfaces/DecryptedTransferPayload'
export * from './Interfaces/DeletedTransferPayload'
export * from './Interfaces/EncryptedTransferPayload'
export * from './Interfaces/TransferPayload'
export * from './Interfaces/TypeCheck'