feat: add models package
This commit is contained in:
75
packages/models/src/Domain/Syncable/File/File.spec.ts
Normal file
75
packages/models/src/Domain/Syncable/File/File.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ConflictStrategy } from './../../Abstract/Item/Types/ConflictStrategy'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||
import { FileContent, FileItem } from './File'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
|
||||
UuidGenerator.SetGenerator(() => String(Math.random()))
|
||||
|
||||
describe('file', () => {
|
||||
const createFile = (content: Partial<FileContent> = {}): FileItem => {
|
||||
return new FileItem(
|
||||
new DecryptedPayload<FileContent>({
|
||||
uuid: '123',
|
||||
content_type: ContentType.File,
|
||||
content: FillItemContent<FileContent>({
|
||||
name: 'name.png',
|
||||
key: 'secret',
|
||||
remoteIdentifier: 'A',
|
||||
encryptionHeader: 'header',
|
||||
encryptedChunkSizes: [1, 2, 3],
|
||||
...content,
|
||||
}),
|
||||
dirty: true,
|
||||
...PayloadTimestampDefaults(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const copyFile = (file: FileItem, override: Partial<FileContent> = {}): FileItem => {
|
||||
return new FileItem(
|
||||
file.payload.copy({
|
||||
content: {
|
||||
...file.content,
|
||||
...override,
|
||||
} as FileContent,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it('should not copy on name conflict', () => {
|
||||
const file = createFile({ name: 'file.png' })
|
||||
const conflictedFile = copyFile(file, { name: 'different.png' })
|
||||
|
||||
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBase)
|
||||
})
|
||||
|
||||
it('should copy on key conflict', () => {
|
||||
const file = createFile({ name: 'file.png' })
|
||||
const conflictedFile = copyFile(file, { key: 'different-secret' })
|
||||
|
||||
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
|
||||
})
|
||||
|
||||
it('should copy on header conflict', () => {
|
||||
const file = createFile({ name: 'file.png' })
|
||||
const conflictedFile = copyFile(file, { encryptionHeader: 'different-header' })
|
||||
|
||||
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
|
||||
})
|
||||
|
||||
it('should copy on identifier conflict', () => {
|
||||
const file = createFile({ name: 'file.png' })
|
||||
const conflictedFile = copyFile(file, { remoteIdentifier: 'different-identifier' })
|
||||
|
||||
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
|
||||
})
|
||||
|
||||
it('should copy on chunk sizes conflict', () => {
|
||||
const file = createFile({ name: 'file.png' })
|
||||
const conflictedFile = copyFile(file, { encryptedChunkSizes: [10, 9, 8] })
|
||||
|
||||
expect(file.strategyWhenConflictingWithItem(conflictedFile)).toEqual(ConflictStrategy.KeepBaseDuplicateApply)
|
||||
})
|
||||
})
|
||||
85
packages/models/src/Domain/Syncable/File/File.ts
Normal file
85
packages/models/src/Domain/Syncable/File/File.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { FileMetadata } from './FileMetadata'
|
||||
import { FileProtocolV1 } from './FileProtocolV1'
|
||||
import { SortableItem } from '../../Runtime/Collection/CollectionSort'
|
||||
import { ConflictStrategy } from '../../Abstract/Item'
|
||||
|
||||
type EncryptedBytesLength = number
|
||||
type DecryptedBytesLength = number
|
||||
|
||||
interface SizesDeprecatedDueToAmbiguousNaming {
|
||||
size?: DecryptedBytesLength
|
||||
chunkSizes?: EncryptedBytesLength[]
|
||||
}
|
||||
|
||||
interface Sizes {
|
||||
decryptedSize: DecryptedBytesLength
|
||||
encryptedChunkSizes: EncryptedBytesLength[]
|
||||
}
|
||||
|
||||
interface FileContentWithoutSize {
|
||||
remoteIdentifier: string
|
||||
name: string
|
||||
key: string
|
||||
encryptionHeader: string
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
export type FileContentSpecialized = FileContentWithoutSize & FileMetadata & SizesDeprecatedDueToAmbiguousNaming & Sizes
|
||||
|
||||
export type FileContent = FileContentSpecialized & ItemContent
|
||||
|
||||
export class FileItem
|
||||
extends DecryptedItem<FileContent>
|
||||
implements FileContentWithoutSize, Sizes, FileProtocolV1, FileMetadata, SortableItem
|
||||
{
|
||||
public readonly remoteIdentifier: string
|
||||
public readonly name: string
|
||||
public readonly key: string
|
||||
public readonly encryptionHeader: string
|
||||
public readonly mimeType: string
|
||||
|
||||
public readonly decryptedSize: DecryptedBytesLength
|
||||
public readonly encryptedChunkSizes: EncryptedBytesLength[]
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<FileContent>) {
|
||||
super(payload)
|
||||
this.remoteIdentifier = this.content.remoteIdentifier
|
||||
this.name = this.content.name
|
||||
this.key = this.content.key
|
||||
|
||||
if (this.content.size && this.content.chunkSizes) {
|
||||
this.decryptedSize = this.content.size
|
||||
this.encryptedChunkSizes = this.content.chunkSizes
|
||||
} else {
|
||||
this.decryptedSize = this.content.decryptedSize
|
||||
this.encryptedChunkSizes = this.content.encryptedChunkSizes
|
||||
}
|
||||
|
||||
this.encryptionHeader = this.content.encryptionHeader
|
||||
this.mimeType = this.content.mimeType
|
||||
}
|
||||
|
||||
public override strategyWhenConflictingWithItem(item: FileItem): ConflictStrategy {
|
||||
if (
|
||||
item.key !== this.key ||
|
||||
item.encryptionHeader !== this.encryptionHeader ||
|
||||
item.remoteIdentifier !== this.remoteIdentifier ||
|
||||
JSON.stringify(item.encryptedChunkSizes) !== JSON.stringify(this.encryptedChunkSizes)
|
||||
) {
|
||||
return ConflictStrategy.KeepBaseDuplicateApply
|
||||
}
|
||||
|
||||
return ConflictStrategy.KeepBase
|
||||
}
|
||||
|
||||
public get encryptedSize(): number {
|
||||
return this.encryptedChunkSizes.reduce((total, chunk) => total + chunk, 0)
|
||||
}
|
||||
|
||||
public get title(): string {
|
||||
return this.name
|
||||
}
|
||||
}
|
||||
4
packages/models/src/Domain/Syncable/File/FileMetadata.ts
Normal file
4
packages/models/src/Domain/Syncable/File/FileMetadata.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface FileMetadata {
|
||||
name: string
|
||||
mimeType: string
|
||||
}
|
||||
33
packages/models/src/Domain/Syncable/File/FileMutator.ts
Normal file
33
packages/models/src/Domain/Syncable/File/FileMutator.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { SNNote } from '../Note/Note'
|
||||
import { FileContent } from './File'
|
||||
import { FileToNoteReference } from '../../Abstract/Reference/FileToNoteReference'
|
||||
import { ContenteReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
|
||||
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
|
||||
|
||||
export class FileMutator extends DecryptedItemMutator<FileContent> {
|
||||
set name(newName: string) {
|
||||
this.mutableContent.name = newName
|
||||
}
|
||||
|
||||
set encryptionHeader(encryptionHeader: string) {
|
||||
this.mutableContent.encryptionHeader = encryptionHeader
|
||||
}
|
||||
|
||||
public addNote(note: SNNote): void {
|
||||
const reference: FileToNoteReference = {
|
||||
reference_type: ContenteReferenceType.FileToNote,
|
||||
content_type: ContentType.Note,
|
||||
uuid: note.uuid,
|
||||
}
|
||||
|
||||
const references = this.mutableContent.references || []
|
||||
references.push(reference)
|
||||
this.mutableContent.references = references
|
||||
}
|
||||
|
||||
public removeNote(note: SNNote): void {
|
||||
const references = this.immutableItem.references.filter((ref) => ref.uuid !== note.uuid)
|
||||
this.mutableContent.references = references
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface FileProtocolV1 {
|
||||
readonly encryptionHeader: string
|
||||
readonly key: string
|
||||
readonly remoteIdentifier: string
|
||||
}
|
||||
|
||||
export enum FileProtocolV1Constants {
|
||||
KeySize = 256,
|
||||
}
|
||||
4
packages/models/src/Domain/Syncable/File/index.ts
Normal file
4
packages/models/src/Domain/Syncable/File/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './File'
|
||||
export * from './FileMutator'
|
||||
export * from './FileMetadata'
|
||||
export * from './FileProtocolV1'
|
||||
Reference in New Issue
Block a user