diff --git a/packages/core/__tests__/backup.test.js b/packages/core/__tests__/backup.test.js index 15b0e3949..f257205be 100644 --- a/packages/core/__tests__/backup.test.js +++ b/packages/core/__tests__/backup.test.js @@ -174,6 +174,8 @@ describe.each([ db.attachments.all.every((v) => v.dateModified > 0 && !v.dateEdited) ).toBeTruthy(); + expect(db.attachments.all.every((a) => !a.noteIds)).toBeTruthy(); + if (data.data.settings.pins) expect(db.shortcuts.all).toHaveLength(data.data.settings.pins.length); diff --git a/packages/core/src/api/sync/merger.ts b/packages/core/src/api/sync/merger.ts index 511be0bdb..5a6b163c9 100644 --- a/packages/core/src/api/sync/merger.ts +++ b/packages/core/src/api/sync/merger.ts @@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { set } from "../../utils/set"; import { logger } from "../../logger"; import { isHTMLEqual } from "../../utils/html-diff"; import Database from ".."; @@ -201,9 +200,8 @@ class Merger { break; } case "attachment": { - if (isDeleted(remoteItem)) { - return this.db.attachments.merge(undefined, remoteItem); - } + if (isDeleted(remoteItem)) return remoteItem; + if (remoteItem.type !== "attachment") return; const localAttachment = this.db.attachments.attachment( @@ -213,7 +211,6 @@ class Merger { localAttachment && localAttachment.dateUploaded !== remoteItem.dateUploaded ) { - const noteIds = localAttachment.noteIds.slice(); const isRemoved = await this.db.attachments.remove( localAttachment.metadata.hash, true @@ -222,9 +219,8 @@ class Merger { throw new Error( "Conflict could not be resolved in one of the attachments." ); - remoteItem.noteIds = set.union(remoteItem.noteIds, noteIds); } - return this.db.attachments.merge(undefined, remoteItem); + return remoteItem; } } } diff --git a/packages/core/src/collections/attachments.ts b/packages/core/src/collections/attachments.ts index 1dcfda2b9..e95e0ada9 100644 --- a/packages/core/src/collections/attachments.ts +++ b/packages/core/src/collections/attachments.ts @@ -19,11 +19,10 @@ along with this program. If not, see . import { ICollection } from "./collection"; import { getId } from "../utils/id"; -import { deleteItem, hasItem } from "../utils/array"; +import { hasItem } from "../utils/array"; import { EV, EVENTS } from "../common"; import dataurl from "../utils/dataurl"; import dayjs from "dayjs"; -import { set } from "../utils/set"; import { getFileNameWithExtension, isImage, @@ -32,12 +31,7 @@ import { import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto"; import { CachedCollection } from "../database/cached-collection"; import { Output } from "../interfaces"; -import { - Attachment, - AttachmentMetadata, - MaybeDeletedItem, - isDeleted -} from "../types"; +import { Attachment, AttachmentMetadata, isDeleted } from "../types"; import Database from "../api"; export class Attachments implements ICollection { @@ -108,27 +102,6 @@ export class Attachments implements ICollection { await this.collection.init(); } - merge( - localAttachment: MaybeDeletedItem | undefined, - remoteAttachment: MaybeDeletedItem - ) { - if (isDeleted(remoteAttachment)) return remoteAttachment; - - if ( - localAttachment && - !isDeleted(localAttachment) && - localAttachment.noteIds - ) { - remoteAttachment.noteIds = set.union( - remoteAttachment.noteIds, - localAttachment.noteIds - ); - remoteAttachment.remote = false; - } - - return remoteAttachment; - } - async add( item: Partial< Omit & { @@ -145,7 +118,6 @@ export class Attachments implements ICollection { (a) => a.metadata.hash === item.metadata?.hash ); const id = oldAttachment?.id || getId(); - const noteIds = set.union(oldAttachment?.noteIds || [], item.noteIds || []); const encryptedKey = item.key ? await this.encryptKey(item.key) @@ -154,7 +126,6 @@ export class Attachments implements ICollection { ...oldAttachment, ...oldAttachment?.metadata, ...item, - noteIds, key: encryptedKey }; @@ -193,7 +164,6 @@ export class Attachments implements ICollection { return this.collection.add({ type: "attachment", id, - noteIds, iv, salt, length, @@ -226,21 +196,11 @@ export class Attachments implements ICollection { return JSON.parse(plainData); } - async delete(hashOrId: string, noteId: string) { - const attachment = this.attachment(hashOrId); - if (!attachment || !deleteItem(attachment.noteIds, noteId)) return; - if (!attachment.noteIds.length) { - attachment.dateDeleted = Date.now(); - EV.publish(EVENTS.attachmentDeleted, attachment); - } - return await this.collection.update(attachment); - } - async remove(hashOrId: string, localOnly: boolean) { const attachment = this.attachment(hashOrId); if (!attachment || !attachment.metadata) return false; - if (!localOnly && !(await this._canDetach(attachment))) + if (!localOnly && !(await this.canDetach(attachment))) throw new Error("This attachment is inside a locked note."); if ( @@ -261,38 +221,37 @@ export class Attachments implements ICollection { } async detach(attachment: Attachment) { - for (const noteId of attachment.noteIds) { - const note = this.db.notes.note(noteId); - if (!note || !note.data.contentId) continue; - await this.db.content.removeAttachments(note.data.contentId, [ + for (const note of this.db.relations.from(attachment, "note").resolved()) { + if (!note || !note.contentId) continue; + await this.db.content.removeAttachments(note.contentId, [ attachment.metadata.hash ]); } } - async _canDetach(attachment: Attachment) { - for (const noteId of attachment.noteIds) { - const note = this.db.notes?.note(noteId); - if (note && note.data.locked) return false; - } - - return true; + private async canDetach(attachment: Attachment) { + return this.db.relations + .from(attachment, "note") + .resolved() + .every((note) => !note.locked); } ofNote( noteId: string, - type: "files" | "images" | "webclips" | "all" + ...types: ("files" | "images" | "webclips" | "all")[] ): Attachment[] { - let attachments: Attachment[] = []; + const noteAttachments = this.db.relations + .from({ type: "note", id: noteId }, "attachment") + .resolved(); - if (type === "files") attachments = this.files; - else if (type === "images") attachments = this.images; - else if (type === "webclips") attachments = this.webclips; - else if (type === "all") attachments = this.all; + if (types.includes("all")) return noteAttachments; - return attachments.filter((attachment) => - hasItem(attachment.noteIds, noteId) - ); + return noteAttachments.filter((a) => { + if (isImage(a.metadata.type) && types.includes("images")) return true; + else if (isWebClip(a.metadata.type) && types.includes("webclips")) + return true; + else if (types.includes("files")) return true; + }); } exists(hash: string) { @@ -387,12 +346,11 @@ export class Attachments implements ICollection { } async downloadMedia(noteId: string, hashesToLoad?: string[]) { - const attachments = this.media.filter( - (attachment) => - !!attachment.metadata && - hasItem(attachment.noteIds, noteId) && - (!hashesToLoad || hasItem(hashesToLoad, attachment.metadata?.hash)) - ); + let attachments = this.ofNote(noteId, "images", "webclips"); + if (hashesToLoad) + attachments = attachments.filter((a) => + hasItem(hashesToLoad, a.metadata.hash) + ); await this.db.fs().queueDownloads( attachments.map((a) => ({ diff --git a/packages/core/src/collections/content.ts b/packages/core/src/collections/content.ts index efd437b6f..10b8fcc50 100644 --- a/packages/core/src/collections/content.ts +++ b/packages/core/src/collections/content.ts @@ -20,7 +20,6 @@ along with this program. If not, see . import { ICollection } from "./collection"; import { getId } from "../utils/id"; import { getContentFromData } from "../content-types"; -import { hasItem } from "../utils/array"; import { ResolveHashes } from "../content-types/tiptap"; import { isCipher } from "../database/crypto"; import { @@ -238,16 +237,15 @@ export class Content implements ICollection { async extractAttachments(contentItem: UnencryptedContentItem) { if (contentItem.localOnly) return contentItem; - const allAttachments = this.db.attachments?.all; const content = getContentFromData(contentItem.type, contentItem.data); if (!content) return contentItem; const { data, hashes } = await content.extractAttachments( this.db.attachments.save ); - const noteAttachments = allAttachments.filter((attachment) => - hasItem(attachment.noteIds, contentItem.noteId) - ); + const noteAttachments = this.db.relations + .from({ type: "note", id: contentItem.noteId }, "attachment") + .resolved(); const toDelete = noteAttachments.filter((attachment) => { return hashes.every((hash) => hash !== attachment.metadata.hash); @@ -258,17 +256,25 @@ export class Content implements ICollection { }); for (const attachment of toDelete) { - await this.db.attachments.delete( - attachment.metadata.hash, - contentItem.noteId + await this.db.relations.unlink( + { + id: contentItem.noteId, + type: "note" + }, + attachment ); } for (const hash of toAdd) { - await this.db.attachments.add({ - noteIds: [contentItem.noteId], - metadata: { hash } - }); + const attachment = this.db.attachments.attachment(hash); + if (!attachment) continue; + await this.db.relations.add( + { + id: contentItem.noteId, + type: "note" + }, + attachment + ); } if (toAdd.length > 0) { diff --git a/packages/core/src/collections/notes.ts b/packages/core/src/collections/notes.ts index dcbb2ff57..22f5ded5c 100644 --- a/packages/core/src/collections/notes.ts +++ b/packages/core/src/collections/notes.ts @@ -302,11 +302,7 @@ export class Notes implements ICollection { await this.db.relations.unlinkAll(item, "tag"); await this.db.relations.unlinkAll(item, "color"); - - const attachments = this.db.attachments.ofNote(itemData.id, "all"); - for (const attachment of attachments) { - await this.db.attachments.delete(attachment.metadata.hash, itemData.id); - } + await this.db.relations.unlinkAll(item, "attachment"); if (moveToTrash && !isTrashItem(itemData)) await this.db.trash.add(itemData); diff --git a/packages/core/src/collections/relations.ts b/packages/core/src/collections/relations.ts index 9ba70a8c2..2366e56e0 100644 --- a/packages/core/src/collections/relations.ts +++ b/packages/core/src/collections/relations.ts @@ -168,6 +168,12 @@ export class Relations implements ICollection { item = notebook.data; break; } + case "attachment": { + const attachment = this.db.attachments.attachment(reference.id); + if (!attachment) continue; + item = attachment; + break; + } } if (item) items.push(item); } diff --git a/packages/core/src/database/cached-collection.ts b/packages/core/src/database/cached-collection.ts index 1c6a13726..410a6202e 100644 --- a/packages/core/src/database/cached-collection.ts +++ b/packages/core/src/database/cached-collection.ts @@ -20,9 +20,9 @@ along with this program. If not, see . import { IndexedCollection } from "./indexed-collection"; import MapStub from "../utils/map"; import { - BaseItem, CollectionType, Collections, + ItemMap, MaybeDeletedItem, isDeleted } from "../types"; @@ -32,7 +32,7 @@ import { chunkedIterate } from "../utils/array"; export class CachedCollection< TCollectionType extends CollectionType, - T extends BaseItem + T extends ItemMap[Collections[TCollectionType]] > { private collection: IndexedCollection; private cache = new Map>(); diff --git a/packages/core/src/migrations.ts b/packages/core/src/migrations.ts index 8bc856542..f26a0f1a9 100644 --- a/packages/core/src/migrations.ts +++ b/packages/core/src/migrations.ts @@ -249,6 +249,16 @@ const migrations: Migration[] = [ delete item.tags; delete item.color; return true; + }, + attachment: async (item, db) => { + for (const noteId of item.noteIds || []) { + await db.relations.add( + { type: "attachment", id: item.id }, + { type: "note", id: noteId } + ); + } + delete item.noteIds; + return true; } } }, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c7fd1bfb9..be4f54627 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -178,7 +178,6 @@ export type AttachmentMetadata = { }; export interface Attachment extends BaseItem<"attachment"> { - noteIds: string[]; iv: string; salt: string; length: number; @@ -189,6 +188,11 @@ export interface Attachment extends BaseItem<"attachment"> { dateUploaded?: number; failed?: string; dateDeleted?: number; + + /** + * @deprecated only kept here for migration purposes + */ + noteIds?: string[]; } export interface Color extends BaseItem<"color"> {