feat: add models package
This commit is contained in:
51
packages/models/src/Domain/Abstract/Content/ItemContent.ts
Normal file
51
packages/models/src/Domain/Abstract/Content/ItemContent.ts
Normal 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)
|
||||
}
|
||||
60
packages/models/src/Domain/Abstract/Contextual/BackupFile.ts
Normal file
60
packages/models/src/Domain/Abstract/Contextual/BackupFile.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
107
packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts
Normal file
107
packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
10
packages/models/src/Domain/Abstract/Contextual/index.ts
Normal file
10
packages/models/src/Domain/Abstract/Contextual/index.ts
Normal 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'
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum ConflictStrategy {
|
||||
KeepBase = 1,
|
||||
KeepApply = 2,
|
||||
KeepBaseDuplicateApply = 3,
|
||||
DuplicateBaseKeepApply = 4,
|
||||
KeepBaseMergeRefs = 5,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum SingletonStrategy {
|
||||
KeepEarliest = 1,
|
||||
}
|
||||
29
packages/models/src/Domain/Abstract/Item/index.ts
Normal file
29
packages/models/src/Domain/Abstract/Item/index.ts
Normal 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'
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
13
packages/models/src/Domain/Abstract/Payload/index.ts
Normal file
13
packages/models/src/Domain/Abstract/Payload/index.ts
Normal 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'
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ContenteReferenceType } from './ContenteReferenceType'
|
||||
|
||||
export interface AnonymousReference {
|
||||
uuid: string
|
||||
content_type: ContentType
|
||||
reference_type: ContenteReferenceType
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
|
||||
import { Reference } from './Reference'
|
||||
|
||||
export type ContentReference = LegacyAnonymousReference | Reference
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum ContenteReferenceType {
|
||||
TagToParentTag = 'TagToParentTag',
|
||||
FileToNote = 'FileToNote',
|
||||
TagToFile = 'TagToFile',
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
30
packages/models/src/Domain/Abstract/Reference/Functions.ts
Normal file
30
packages/models/src/Domain/Abstract/Reference/Functions.ts
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface LegacyAnonymousReference {
|
||||
uuid: string
|
||||
content_type: string
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
|
||||
|
||||
export interface LegacyTagToNoteReference extends LegacyAnonymousReference {
|
||||
content_type: ContentType.Note
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TagToParentTagReference } from './TagToParentTagReference'
|
||||
|
||||
export type Reference = TagToParentTagReference
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ItemContent } from '../../Content/ItemContent'
|
||||
import { TransferPayload } from './TransferPayload'
|
||||
|
||||
export interface DecryptedTransferPayload<C extends ItemContent = ItemContent> extends TransferPayload {
|
||||
content: C
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { TransferPayload } from './TransferPayload'
|
||||
|
||||
export interface DeletedTransferPayload extends TransferPayload {
|
||||
content: undefined
|
||||
deleted: true
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './Interfaces/DecryptedTransferPayload'
|
||||
export * from './Interfaces/DeletedTransferPayload'
|
||||
export * from './Interfaces/EncryptedTransferPayload'
|
||||
export * from './Interfaces/TransferPayload'
|
||||
export * from './Interfaces/TypeCheck'
|
||||
Reference in New Issue
Block a user