diff --git a/packages/core/__tests__/backup.test.js b/packages/core/__tests__/backup.test.js index f257205be..6ef919a17 100644 --- a/packages/core/__tests__/backup.test.js +++ b/packages/core/__tests__/backup.test.js @@ -166,9 +166,7 @@ describe.each([ db.notebooks.all.every((v) => v.title != null && v.dateModified > 0) ).toBeTruthy(); - expect( - db.notebooks.all.every((v) => v.topics.every((topic) => !topic.notes)) - ).toBeTruthy(); + expect(db.notebooks.all.every((v) => !v.topics)).toBeTruthy(); expect( db.attachments.all.every((v) => v.dateModified > 0 && !v.dateEdited) diff --git a/packages/core/__tests__/lookup.test.js b/packages/core/__tests__/lookup.test.js index 51264bd02..324a6101a 100644 --- a/packages/core/__tests__/lookup.test.js +++ b/packages/core/__tests__/lookup.test.js @@ -64,10 +64,3 @@ test("search notebooks", () => let filtered = db.lookup.notebooks(db.notebooks.all, "Description"); expect(filtered.length).toBeGreaterThan(0); })); - -test("search topics", () => - notebookTest().then(async ({ db, id }) => { - const topics = db.notebooks.topics(id).all; - let filtered = db.lookup.topics(topics, "hello"); - expect(filtered).toHaveLength(1); - })); diff --git a/packages/core/__tests__/notebooks.test.js b/packages/core/__tests__/notebooks.test.js index 4bf681163..37379b72f 100644 --- a/packages/core/__tests__/notebooks.test.js +++ b/packages/core/__tests__/notebooks.test.js @@ -17,10 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { notebookTest, TEST_NOTEBOOK, TEST_NOTE, delay } from "./utils"; -import { makeTopic } from "../src/collections/topics"; +import { notebookTest, TEST_NOTEBOOK } from "./utils"; import { test, expect } from "vitest"; -import qclone from "qclone"; test("add a notebook", () => notebookTest().then(({ db, id }) => { @@ -58,135 +56,3 @@ test("updating notebook with empty title should throw", () => notebookTest().then(async ({ db, id }) => { await expect(db.notebooks.add({ id, title: "" })).rejects.toThrow(); })); - -test("merge notebook with new topics", () => - notebookTest().then(async ({ db, id }) => { - let notebook = db.notebooks.notebook(id); - - const newNotebook = db.notebooks.merge(notebook.data, { - ...notebook.data, - topics: [...notebook.data.topics, makeTopic({ title: "Home" }, id)], - remote: true - }); - - expect( - newNotebook.topics.findIndex((t) => t.title === "Home") - ).toBeGreaterThanOrEqual(0); - expect( - newNotebook.topics.findIndex((t) => t.title === "hello") - ).toBeGreaterThanOrEqual(0); - })); - -test("merge notebook with topics removed", () => - notebookTest().then(async ({ db, id }) => { - let notebook = db.notebooks.notebook(id); - - const newNotebook = db.notebooks.merge(notebook.data, { - ...notebook.data, - topics: [makeTopic({ title: "Home" }, id)], - remote: true - }); - - expect( - newNotebook.topics.findIndex((t) => t.title === "Home") - ).toBeGreaterThanOrEqual(0); - expect( - newNotebook.topics.findIndex((t) => t.title === "hello") - ).toBeLessThan(0); - })); - -test("merge notebook with topic edited", () => - notebookTest().then(async ({ db, id }) => { - let notebook = db.notebooks.notebook(id); - - const newNotebook = db.notebooks.merge(notebook.data, { - ...notebook.data, - topics: [{ ...notebook.data.topics[0], title: "hello (edited)" }], - remote: true - }); - - expect( - newNotebook.topics.findIndex((t) => t.title === "hello (edited)") - ).toBeGreaterThanOrEqual(0); - expect( - newNotebook.topics.findIndex((t) => t.title === "hello") - ).toBeLessThan(0); - })); - -test("merge notebook when local notebook is also edited", () => - notebookTest().then(async ({ db, id }) => { - let notebook = db.notebooks.notebook(id); - - let newNotebook = { ...qclone(notebook.data), remote: true }; - newNotebook.topics[0].title = "hello (edited)"; - - await delay(500); - - await notebook.topics.add({ - ...notebook.topics.all[0], - title: "hello (edited too)" - }); - - newNotebook = db.notebooks.merge( - db.notebooks.notebook(id).data, - newNotebook, - 0 - ); - expect( - newNotebook.topics.findIndex((t) => t.title === "hello (edited too)") - ).toBeGreaterThanOrEqual(0); - expect( - newNotebook.topics.findIndex((t) => t.title === "hello (edited)") - ).toBeLessThan(0); - expect( - newNotebook.topics.findIndex((t) => t.title === "hello") - ).toBeLessThan(0); - })); - -test("merging notebook when local notebook is not edited should not update remote notebook dateEdited", () => - notebookTest().then(async ({ db, id }) => { - let notebook = db.notebooks.notebook(id); - - let note = await db.notes.add(TEST_NOTE); - await db.notes.addToNotebook( - { id: notebook.data.id, topic: notebook.data.topics[0].id }, - note - ); - - const newNotebook = db.notebooks.merge(notebook.data, { - ...notebook.data, - remote: true - }); - - expect(db.notebooks.notebook(id).dateEdited).toBe(newNotebook.dateEdited); - })); - -test("merge notebook with topic removed that is edited in the local notebook", () => - notebookTest().then(async ({ db, id }) => { - let notebook = db.notebooks.notebook(id); - - let newNotebook = { ...qclone(notebook.data), remote: true }; - newNotebook.topics.splice(0, 1); // remove hello topic - - const lastSynced = Date.now(); - - await delay(500); - - await notebook.topics.add({ - ...notebook.topics.all[1], - title: "hello (i exist)" - }); - - newNotebook = db.notebooks.merge( - db.notebooks.notebook(id).data, - newNotebook, - lastSynced - ); - - expect( - newNotebook.topics.findIndex((t) => t.title === "hello (i exist)") - ).toBeGreaterThanOrEqual(0); - expect( - newNotebook.topics.findIndex((t) => t.title === "hello") - ).toBeLessThan(0); - })); diff --git a/packages/core/__tests__/notes.test.ts b/packages/core/__tests__/notes.test.ts index 971df8a39..dc62615af 100644 --- a/packages/core/__tests__/notes.test.ts +++ b/packages/core/__tests__/notes.test.ts @@ -35,35 +35,37 @@ async function createAndAddNoteToNotebook( noteId: string, options: { notebookTitle: string; - topicTitle: string; + subNotebookTitle: string; } ) { - const { notebookTitle, topicTitle } = options; + const { notebookTitle, subNotebookTitle } = options; const notebookId = await db.notebooks.add({ title: notebookTitle }); if (!notebookId) throw new Error("Could not create notebook"); - const topics = db.notebooks.topics(notebookId); - await topics.add({ title: topicTitle }); + const subNotebookId = await db.notebooks.add({ title: subNotebookTitle }); + if (!subNotebookId) throw new Error("Could not create sub notebook"); - const topic = topics.topic(topicTitle); - if (!topic) throw new Error("Could not find topic."); - await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, noteId); + await db.relations.add( + { type: "notebook", id: notebookId }, + { type: "notebook", id: subNotebookId } + ); - return { topic, topics, notebookId }; + await db.notes.addToNotebook(subNotebookId, noteId); + + return { subNotebookId, notebookId }; } test("add invalid note", () => databaseTest().then(async (db) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - let id = await db.notes.add(); - expect(id).toBeUndefined(); - id = await db.notes.add({}); - expect(id).toBeUndefined(); + expect(db.notes.add()).rejects.toThrow(); + + expect(db.notes.add({})).rejects.toThrow(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - id = await db.notes.add({ hello: "world" }); - expect(id).toBeUndefined(); + expect(db.notes.add({ hello: "world" })).rejects.toThrow(); })); test("add note", () => @@ -84,23 +86,21 @@ test("delete note", () => const notebookId = await db.notebooks.add(TEST_NOTEBOOK); if (!notebookId) throw new Error("Could not create notebook."); - const topics = db.notebooks.topics(notebookId); + const subNotebookId = await db.notebooks.add({ title: "hello" }); + if (!notebookId) throw new Error("Could not create sub notebook."); - let topic = topics.topic("hello"); - if (!topic) throw new Error("Could not find topic."); + await db.relations.add( + { type: "notebook", id: notebookId }, + { type: "notebook", id: subNotebookId } + ); - await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id); + await db.notes.addToNotebook(subNotebookId, id); - topic = topics.topic("hello"); - if (!topic) throw new Error("Could not find topic."); - - expect(topic.all.findIndex((v) => v.id === id)).toBeGreaterThan(-1); await db.notes.delete(id); - expect(db.notes.note(id)).toBeUndefined(); - expect(topic.all.findIndex((v) => v.id === id)).toBe(-1); + expect(db.notes.note(id)).toBeUndefined(); expect(db.notebooks.totalNotes(notebookId)).toBe(0); - expect(topics.topic("hello")?.totalNotes).toBe(0); + expect(db.notebooks.totalNotes(subNotebookId)).toBe(0); })); test("get all notes", () => @@ -217,57 +217,67 @@ test("favorite note", () => expect(note?.data.favorite).toBe(true); })); -test("add note to topic", () => +test("add note to subnotebook", () => noteTest().then(async ({ db, id }) => { - const { topic, notebookId } = await createAndAddNoteToNotebook(db, id, { - notebookTitle: "Hello", - topicTitle: "Home" - }); - - expect(topic.all).toHaveLength(1); - expect(topic.totalNotes).toBe(1); - expect(db.notebooks.totalNotes(notebookId)).toBe(1); - expect(db.notes.note(id)?.notebooks?.some((n) => n.id === notebookId)).toBe( - true + const { subNotebookId, notebookId } = await createAndAddNoteToNotebook( + db, + id, + { + notebookTitle: "Hello", + subNotebookTitle: "Home" + } ); + + expect( + db.relations.from({ type: "notebook", id: notebookId }, "notebook") + ).toHaveLength(1); + expect(db.notebooks.totalNotes(subNotebookId)).toBe(1); + expect(db.notebooks.totalNotes(notebookId)).toBe(1); })); test("duplicate note to topic should not be added", () => noteTest().then(async ({ db, id }) => { - const { topics } = await createAndAddNoteToNotebook(db, id, { + const { subNotebookId } = await createAndAddNoteToNotebook(db, id, { notebookTitle: "Hello", - topicTitle: "Home" + subNotebookTitle: "Home" }); - expect(topics.topic("Home")?.all).toHaveLength(1); + expect(db.notebooks.totalNotes(subNotebookId)).toBe(1); })); test("add the same note to 2 notebooks", () => noteTest().then(async ({ db, id }) => { const nb1 = await createAndAddNoteToNotebook(db, id, { notebookTitle: "Hello", - topicTitle: "Home" + subNotebookTitle: "Home" }); const nb2 = await createAndAddNoteToNotebook(db, id, { notebookTitle: "Hello2", - topicTitle: "Home2" + subNotebookTitle: "Home2" }); - expect(nb1.topics.topic(nb1.topic.id)?.has(id)).toBe(true); - expect(nb2.topics.topic(nb2.topic.id)?.has(id)).toBe(true); - expect(db.notes.note(id)?.notebooks).toHaveLength(2); + expect( + db.relations + .from({ type: "notebook", id: nb1.subNotebookId }, "note") + .has(id) + ).toBe(true); + expect( + db.relations + .from({ type: "notebook", id: nb2.subNotebookId }, "note") + .has(id) + ).toBe(true); + expect(db.relations.to({ type: "note", id }, "notebook")).toHaveLength(2); })); test("moving note to same notebook and topic should do nothing", () => noteTest().then(async ({ db, id }) => { - const { notebookId, topic } = await createAndAddNoteToNotebook(db, id, { + const { subNotebookId } = await createAndAddNoteToNotebook(db, id, { notebookTitle: "Home", - topicTitle: "Hello" + subNotebookTitle: "Hello" }); - await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id); - expect(db.notes.note(id)?.notebooks?.some((n) => n.id === notebookId)).toBe( - true - ); + await db.notes.addToNotebook(subNotebookId, id); + + expect(db.relations.to({ type: "note", id }, "notebook")).toHaveLength(1); })); test("export note to html", () => @@ -308,19 +318,14 @@ test("deleting a colored note should remove it from that color", () => ); expect( - db.relations - .from({ id: colorId, type: "color" }, "note") - .findIndex((r) => r.to.id === id) - ).toBe(0); + db.relations.from({ id: colorId, type: "color" }, "note").has(id) + ).toBe(true); await db.notes.delete(id); expect( - db.relations - .from({ id: colorId, type: "color" }, "note") - .findIndex((r) => r.to.id === id) - ).toBe(-1); - // TODO expect(color.noteIds.indexOf(id)).toBe(-1); + db.relations.from({ id: colorId, type: "color" }, "note").has(id) + ).toBe(false); })); test("note's content should follow note's localOnly property", () => @@ -330,7 +335,7 @@ test("note's content should follow note's localOnly property", () => if (!note?.contentId) throw new Error("No content in note."); expect(note?.data.localOnly).toBe(true); - let content = await db.content.raw(note.contentId); + let content = await db.content.get(note.contentId); expect(content?.localOnly).toBe(true); await db.notes.note(id)?.localOnly(); @@ -338,7 +343,7 @@ test("note's content should follow note's localOnly property", () => if (!note?.contentId) throw new Error("No content in note."); expect(note?.data.localOnly).toBe(false); - content = await db.content.raw(note.contentId); + content = await db.content.get(note.contentId); expect(content?.localOnly).toBe(false); })); diff --git a/packages/core/__tests__/shortcuts.test.js b/packages/core/__tests__/shortcuts.test.js index 82489104d..8f06aaf61 100644 --- a/packages/core/__tests__/shortcuts.test.js +++ b/packages/core/__tests__/shortcuts.test.js @@ -43,18 +43,6 @@ test("create a duplicate shortcut of notebook", () => expect(db.shortcuts.all[0].item.id).toBe(id); })); -test("create shortcut of a topic", () => - notebookTest().then(async ({ db, id }) => { - const notebook = db.notebooks.notebook(id).data; - const topic = notebook.topics[0]; - await db.shortcuts.add({ - item: { type: "topic", id: topic.id, notebookId: id } - }); - - expect(db.shortcuts.all).toHaveLength(1); - expect(db.shortcuts.all[0].item.id).toBe(topic.id); - })); - test("pin a tag", () => databaseTest().then(async (db) => { const tagId = await db.tags.add({ title: "HELLO!" }); diff --git a/packages/core/__tests__/topics.test.js b/packages/core/__tests__/topics.test.js deleted file mode 100644 index 9ef8b2e8e..000000000 --- a/packages/core/__tests__/topics.test.js +++ /dev/null @@ -1,155 +0,0 @@ -/* -This file is part of the Notesnook project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -import { delay, notebookTest, TEST_NOTE } from "./utils"; -import { test, expect } from "vitest"; - -test("get empty topic", () => - notebookTest().then(({ db, id }) => { - let topic = db.notebooks.topics(id).topic("hello"); - expect(topic.all).toHaveLength(0); - })); - -test("getting invalid topic should return undefined", () => - notebookTest().then(({ db, id }) => { - expect(db.notebooks.topics(id).topic("invalid")).toBeUndefined(); - })); - -test("add topic to notebook", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - await topics.add({ title: "Home" }); - expect(topics.all.length).toBeGreaterThan(1); - expect(topics.all.findIndex((v) => v.title === "Home")).toBeGreaterThan(-1); - })); - -test("add note to topic", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - let topic = topics.topic("hello"); - let noteId = await db.notes.add(TEST_NOTE); - await db.notes.addToNotebook({ id, topic: topic.id }, noteId); - - topic = topics.topic("hello"); - expect(topic.totalNotes).toBe(1); - expect(db.notebooks.notebook(id).totalNotes).toBe(1); - })); - -test("delete note of a topic", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - let topic = topics.topic("hello"); - let noteId = await db.notes.add(TEST_NOTE); - await db.notes.addToNotebook({ id, topic: topic.id }, noteId); - - topic = topics.topic("hello"); - expect(topic.totalNotes).toBe(1); - expect(db.notebooks.notebook(id).totalNotes).toBe(1); - - await db.notes.removeFromNotebook({ id, topic: topic.id }, noteId); - - topic = topics.topic("hello"); - expect(topic.totalNotes).toBe(0); - expect(db.notebooks.notebook(id).totalNotes).toBe(0); - })); - -test("edit topic title", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - - await topics.add({ title: "Home" }); - - let topic = topics.topic("Home"); - - expect(topics.all).toHaveLength(2); - - const oldDateEdited = topic._topic.dateEdited; - - await delay(30); - - await topics.add({ id: topic._topic.id, title: "Hello22" }); - - expect(topics.all).toHaveLength(2); - expect(topics.topic(topic._topic.id)._topic.title).toBe("Hello22"); - expect(topics.topic(topic._topic.id)._topic.dateEdited).toBeGreaterThan( - oldDateEdited - ); - })); - -test("get topic", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - await topics.add({ title: "Home" }); - let topic = topics.topic("Home"); - let noteId = await db.notes.add({ - content: TEST_NOTE.content - }); - await db.notes.addToNotebook({ id, topic: topic.id }, noteId); - - topic = topics.topic("Home"); - expect(await db.content.get(topic.all[0].contentId)).toBeDefined(); - expect(topic.totalNotes).toBe(1); - })); - -test("delete a topic", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - await topics.add({ title: "Home" }); - await topics.delete(topics.topic("Home").id); - expect(topics.all.findIndex((v) => v.title === "Home")).toBe(-1); - })); - -test("delete note from edited topic", () => - notebookTest().then(async ({ db, id }) => { - const noteId = await db.notes.add(TEST_NOTE); - let topics = db.notebooks.topics(id); - await topics.add({ title: "Home" }); - let topic = topics.topic("Home"); - await db.notes.addToNotebook({ id, topic: topic._topic.title }, noteId); - await topics.add({ id: topic._topic.id, title: "Hello22" }); - await db.notes.delete(noteId); - })); - -test("editing one topic should not update dateEdited of all", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - - await topics.add({ title: "Home" }); - await topics.add("Home2"); - await topics.add("Home3"); - - let topic = topics.topic("Home"); - - const oldTopics = topics.all.filter((t) => t.title !== "Home"); - - await delay(100); - - await topics.add({ id: topic._topic.id, title: "Hello22" }); - - const newTopics = topics.all.filter((t) => t.title !== "Hello22"); - - expect( - newTopics.every( - (t) => - oldTopics.findIndex( - (topic) => topic.id === t.id && topic.dateEdited === t.dateEdited - ) > -1 - ) - ).toBe(true); - })); diff --git a/packages/core/__tests__/trash.test.js b/packages/core/__tests__/trash.test.ts similarity index 63% rename from packages/core/__tests__/trash.test.js rename to packages/core/__tests__/trash.test.ts index 10b9a0706..d55543f78 100644 --- a/packages/core/__tests__/trash.test.js +++ b/packages/core/__tests__/trash.test.ts @@ -34,8 +34,12 @@ test("trash should be empty", () => test("permanently delete a note", () => databaseTest().then(async (db) => { - const noteId = await db.notes.add({ ...TEST_NOTE, sessionId: Date.now() }); + const noteId = await db.notes.add({ + ...TEST_NOTE, + sessionId: Date.now().toString() + }); const note = db.notes.note(noteId); + if (!note) throw new Error("Could not find note."); let sessions = await db.noteHistory.get(noteId); expect(sessions).toHaveLength(1); @@ -45,7 +49,7 @@ test("permanently delete a note", () => expect(await note.content()).toBeDefined(); await db.trash.delete(db.trash.all[0].id); expect(db.trash.all).toHaveLength(0); - const content = await db.content.get(note.data.contentId); + const content = note.contentId && (await db.content.get(note.contentId)); expect(content).toBeUndefined(); sessions = await db.noteHistory.get(noteId); @@ -54,25 +58,27 @@ test("permanently delete a note", () => test("restore a deleted note that was in a notebook", () => noteTest().then(async ({ db, id }) => { - let nbId = await db.notebooks.add(TEST_NOTEBOOK); - const topic = db.notebooks.notebook(nbId).topics.topic("hello"); - await db.notes.addToNotebook({ id: nbId, topic: topic.id }, id); + const notebookId = await db.notebooks.add(TEST_NOTEBOOK); + const subNotebookId = await db.notebooks.add({ title: "hello" }); + + await db.relations.add( + { type: "notebook", id: notebookId }, + { type: "notebook", id: subNotebookId } + ); + await db.notes.addToNotebook(subNotebookId, id); await db.notes.delete(id); await db.trash.restore(db.trash.all[0].id); expect(db.trash.all).toHaveLength(0); - let note = db.notes.note(id); + const note = db.notes.note(id); expect(note).toBeDefined(); - expect(await note.content()).toBe(TEST_NOTE.content.data); + expect(await note?.content()).toBe(TEST_NOTE.content.data); - const notebook = db.notebooks.notebook(nbId); - expect(notebook.topics.topic(topic.id).has(id)).toBe(true); - - expect(note.notebooks.some((n) => n.id === nbId)).toBe(true); - - expect(notebook.topics.has("hello")).toBeDefined(); + expect( + db.relations.from({ type: "notebook", id: subNotebookId }, "note").has(id) + ).toBe(true); })); test("delete a locked note", () => @@ -82,7 +88,9 @@ test("delete a locked note", () => await db.vault.add(id); await db.notes.delete(id); expect(db.trash.all).toHaveLength(1); - expect(await db.content.get(note.data.contentId)).toBeDefined(); + expect( + note && note.contentId && (await db.content.get(note.contentId)) + ).toBeDefined(); })); test("restore a deleted locked note", () => @@ -92,7 +100,9 @@ test("restore a deleted locked note", () => await db.vault.add(id); await db.notes.delete(id); expect(db.trash.all).toHaveLength(1); - expect(await db.content.get(note.data.contentId)).toBeDefined(); + expect( + note && note.contentId && (await db.content.get(note.contentId)) + ).toBeDefined(); await db.trash.restore(db.trash.all[0].id); expect(db.trash.all).toHaveLength(0); note = db.notes.note(id); @@ -101,69 +111,64 @@ test("restore a deleted locked note", () => test("restore a deleted note that's in a deleted notebook", () => noteTest().then(async ({ db, id }) => { - let nbId = await db.notebooks.add(TEST_NOTEBOOK); - const topic = db.notebooks.notebook(nbId).topics.topic("hello"); - await db.notes.addToNotebook({ id: nbId, topic: topic.id }, id); + const notebookId = await db.notebooks.add(TEST_NOTEBOOK); + await db.notes.addToNotebook(notebookId, id); await db.notes.delete(id); - await db.notebooks.delete(nbId); - const deletedNote = db.trash.all.find( - (v) => v.id === id && v.itemType === "note" - ); - await db.trash.restore(deletedNote.id); - let note = db.notes.note(id); + await db.notebooks.delete(notebookId); + + await db.trash.restore(id); + const note = db.notes.note(id); expect(note).toBeDefined(); - expect(db.notes.note(id).notebook).toBeUndefined(); + expect(db.relations.to({ type: "note", id }, "notebook")).toHaveLength(0); })); test("delete a notebook", () => notebookTest().then(async ({ db, id }) => { - let noteId = await db.notes.add(TEST_NOTE); - let notebook = db.notebooks.notebook(id); - const topic = notebook.topics.topic("hello"); - await db.notes.addToNotebook({ id, topic: topic.id }, noteId); + const noteId = await db.notes.add(TEST_NOTE); + + await db.notes.addToNotebook(id, noteId); await db.notebooks.delete(id); expect(db.notebooks.notebook(id)).toBeUndefined(); - expect(db.notes.note(noteId).notebook).toBeUndefined(); + expect( + db.relations.to({ type: "note", id: noteId }, "notebook") + ).toHaveLength(0); })); test("restore a deleted notebook", () => notebookTest().then(async ({ db, id }) => { - let noteId = await db.notes.add(TEST_NOTE); - const topic = db.notebooks.notebook(id).topics.topic("hello"); - await db.notes.addToNotebook({ id, topic: topic.id }, noteId); + const noteId = await db.notes.add(TEST_NOTE); + await db.notes.addToNotebook(id, noteId); await db.notebooks.delete(id); await db.trash.restore(id); - let notebook = db.notebooks.notebook(id); + const notebook = db.notebooks.notebook(id); expect(notebook).toBeDefined(); - let note = db.notes.note(noteId); - const noteNotebook = note.notebooks.find((n) => n.id === id); - expect(noteNotebook).toBeDefined(); - expect(noteNotebook.topics).toHaveLength(1); - expect(notebook.topics.topic(noteNotebook.topics[0])).toBeDefined(); + expect( + db.relations.to({ type: "note", id: noteId }, "notebook") + ).toHaveLength(1); + expect( + db.relations.to({ type: "note", id: noteId }, "notebook").has(id) + ).toBe(true); })); test("restore a notebook that has deleted notes", () => notebookTest().then(async ({ db, id }) => { - let noteId = await db.notes.add(TEST_NOTE); - - let notebook = db.notebooks.notebook(id); - const topic = notebook.topics.topic("hello"); - await db.notes.addToNotebook({ id, topic: topic.id }, noteId); + const noteId = await db.notes.add(TEST_NOTE); + await db.notes.addToNotebook(id, noteId); await db.notebooks.delete(id); await db.notes.delete(noteId); - const deletedNotebook = db.trash.all.find( - (v) => v.id === id && v.itemType === "notebook" - ); - await db.trash.restore(deletedNotebook.id); - notebook = db.notebooks.notebook(id); + await db.trash.restore(id); + + const notebook = db.notebooks.notebook(id); expect(notebook).toBeDefined(); - expect(notebook.topics.topic("hello").has(noteId)).toBe(false); + expect( + db.relations.from({ type: "notebook", id: id }, "note").has(noteId) + ).toBe(false); })); test("permanently delete items older than 7 days", () => @@ -175,16 +180,27 @@ test("permanently delete items older than 7 days", () => await db.notebooks.delete(notebookId); await db.notes.delete(noteId); + const note = db.trash.all.find((t) => t.id === noteId); + if (!note || note.itemType !== "note") + throw new Error("Could not find note in trash."); + await db.notes.collection.update({ + ...note, type: "trash", id: noteId, dateDeleted: sevenDaysEarlier }); + const notebook = db.trash.all.find((t) => t.id === notebookId); + if (!notebook || notebook.itemType !== "notebook") + throw new Error("Could not find notebook in trash."); + await db.notebooks.collection.update({ + ...notebook, type: "trash", id: notebookId, - dateDeleted: sevenDaysEarlier + dateDeleted: sevenDaysEarlier, + itemType: "notebook" }); expect(db.trash.all).toHaveLength(2); @@ -211,14 +227,17 @@ test("trash cleanup should not delete items newer than 7 days", () => test("clear trash should delete note content", () => databaseTest().then(async (db) => { - const noteId = await db.notes.add({ ...TEST_NOTE, sessionId: Date.now() }); + const noteId = await db.notes.add({ + ...TEST_NOTE, + sessionId: Date.now().toString() + }); const notebookId = await db.notebooks.add(TEST_NOTEBOOK); let sessions = await db.noteHistory.get(noteId); expect(sessions).toHaveLength(1); - let note = { ...db.notes.note(noteId).data }; + const note = { ...db.notes.note(noteId)?.data }; await db.notebooks.delete(notebookId); await db.notes.delete(noteId); @@ -229,9 +248,9 @@ test("clear trash should delete note content", () => expect(db.trash.all).toHaveLength(0); - const content = await db.content.get(note.contentId); + const content = note.contentId && (await db.content.get(note.contentId)); expect(content).toBeUndefined(); - sessions = await db.noteHistory.get(note.id); + sessions = await db.noteHistory.get(noteId); expect(sessions).toHaveLength(0); })); diff --git a/packages/core/__tests__/utils/index.ts b/packages/core/__tests__/utils/index.ts index 4c47faab3..f1c317164 100644 --- a/packages/core/__tests__/utils/index.ts +++ b/packages/core/__tests__/utils/index.ts @@ -26,23 +26,17 @@ import Compressor from "../../__mocks__/compressor.mock"; import { expect } from "vitest"; import { EventSourcePolyfill as EventSource } from "event-source-polyfill"; import { randomBytes } from "../../src/utils/random"; -import { GroupOptions, Note, Notebook, Topic } from "../../src/types"; +import { GroupOptions, Note, Notebook } from "../../src/types"; import { NoteContent } from "../../src/collections/session-content"; -const TEST_NOTEBOOK: Partial< - Omit & { topics: Partial[] } -> = { +const TEST_NOTEBOOK: Partial = { title: "Test Notebook", - description: "Test Description", - topics: [{ title: "hello" }] + description: "Test Description" }; -const TEST_NOTEBOOK2: Partial< - Omit & { topics: Partial[] } -> = { +const TEST_NOTEBOOK2: Partial = { title: "Test Notebook 2", - description: "Test Description 2", - topics: [{ title: "Home2" }] + description: "Test Description 2" }; function databaseTest() { diff --git a/packages/core/src/api/lookup.ts b/packages/core/src/api/lookup.ts index 779545292..695f12955 100644 --- a/packages/core/src/api/lookup.ts +++ b/packages/core/src/api/lookup.ts @@ -26,7 +26,6 @@ import { Notebook, Reminder, Tag, - Topic, TrashItem, isDeleted } from "../types"; @@ -55,16 +54,7 @@ export default class Lookup { } notebooks(array: Notebook[], query: string) { - return search( - array, - query, - (n) => - `${n.title} ${n.description} ${n.topics.map((t) => t.title).join(" ")}` - ); - } - - topics(array: Topic[], query: string) { - return this.byTitle(array, query); + return search(array, query, (n) => `${n.title} ${n.description}}`); } tags(array: GroupedItems, query: string) { diff --git a/packages/core/src/api/sync/index.ts b/packages/core/src/api/sync/index.ts index ab74fb685..713ede51e 100644 --- a/packages/core/src/api/sync/index.ts +++ b/packages/core/src/api/sync/index.ts @@ -335,8 +335,6 @@ class Sync { } async stop(lastSynced: number) { - // refresh topic references - this.db.notes.topicReferences.rebuild(); // refresh monographs on sync completed await this.db.monographs.init(); @@ -373,9 +371,6 @@ class Sync { * @private */ async onPushCompleted(lastSynced: number) { - // refresh topic references - this.db.notes.topicReferences.rebuild(); - this.db.eventManager.publish( EVENTS.databaseSyncRequested, false, diff --git a/packages/core/src/api/sync/merger.ts b/packages/core/src/api/sync/merger.ts index 5a6b163c9..be0d488ce 100644 --- a/packages/core/src/api/sync/merger.ts +++ b/packages/core/src/api/sync/merger.ts @@ -85,7 +85,7 @@ class Merger { ItemMap[TType] | TrashOrItem | TrashOrItem >, type: TType, - lastSynced: number + _lastSynced: number ) { switch (type) { case "shortcut": @@ -93,7 +93,8 @@ class Merger { case "tag": case "color": case "note": - case "relation": { + case "relation": + case "notebook": { const localItem = this.db[SYNC_COLLECTIONS_MAP[type]].collection.getRaw( remoteItem.id ); @@ -102,31 +103,6 @@ class Merger { } break; } - // case "note": { - // const localItem = this.db.notes.collection.getRaw(remoteItem.id); - // if (!localItem || remoteItem.dateModified > localItem.dateModified) { - // return this.db.notes.merge( - // localItem, - // remoteItem as MaybeDeletedItem> - // ); - // } - // break; - // } - case "notebook": { - const THRESHOLD = 1000; - const localItem = this.db.notebooks.collection.getRaw(remoteItem.id); - if ( - !localItem || - this.isConflicted(localItem, remoteItem, lastSynced, THRESHOLD) - ) { - return this.db.notebooks.merge( - localItem, - remoteItem as MaybeDeletedItem>, - lastSynced - ); - } - break; - } } } diff --git a/packages/core/src/collections/notebooks.ts b/packages/core/src/collections/notebooks.ts index 2142ca14d..4c41a9391 100644 --- a/packages/core/src/collections/notebooks.ts +++ b/packages/core/src/collections/notebooks.ts @@ -19,18 +19,9 @@ along with this program. If not, see . import { createNotebookModel } from "../models/notebook"; import { getId } from "../utils/id"; -import { CHECK_IDS, checkIsUserPremium } from "../common"; import { CachedCollection } from "../database/cached-collection"; -import Topics from "./topics"; import Database from "../api"; -import { - MaybeDeletedItem, - Notebook, - Topic, - TrashOrItem, - isDeleted, - isTrashItem -} from "../types"; +import { BaseTrashItem, Notebook, TrashOrItem, isTrashItem } from "../types"; import { ICollection } from "./collection"; export class Notebooks implements ICollection { @@ -51,63 +42,7 @@ export class Notebooks implements ICollection { return this.collection.init(); } - merge( - localNotebook: MaybeDeletedItem> | undefined, - remoteNotebook: MaybeDeletedItem>, - lastSyncedTimestamp: number - ) { - if (isDeleted(remoteNotebook) || isTrashItem(remoteNotebook)) - return remoteNotebook; - - if ( - localNotebook && - (isTrashItem(localNotebook) || isDeleted(localNotebook)) - ) { - if (localNotebook.dateModified > remoteNotebook.dateModified) return; - return remoteNotebook; - } - - if (localNotebook && localNotebook.topics?.length) { - let isChanged = false; - // merge new and old topics - for (const oldTopic of localNotebook.topics) { - const newTopicIndex = remoteNotebook.topics.findIndex( - (t) => t.id === oldTopic.id - ); - const newTopic = remoteNotebook.topics[newTopicIndex]; - - // CASE 1: if topic exists in old notebook but not in new notebook, it's deleted. - // However, if the dateEdited of topic in the old notebook is > lastSyncedTimestamp - // it was newly added or edited so add it to the new notebook. - if (!newTopic && oldTopic.dateEdited > lastSyncedTimestamp) { - remoteNotebook.topics.push({ ...oldTopic, dateEdited: Date.now() }); - isChanged = true; - } - - // CASE 2: if topic exists in new notebook but not in old notebook, it's new. - // This case will be automatically handled as the new notebook is our source of truth. - - // CASE 3: if topic exists in both notebooks: - // if oldTopic.dateEdited > newTopic.dateEdited: we keep oldTopic - // and merge the notes of both topics. - else if (newTopic && oldTopic.dateEdited > newTopic.dateEdited) { - remoteNotebook.topics[newTopicIndex] = { - ...oldTopic, - dateEdited: Date.now() - }; - isChanged = true; - } - } - remoteNotebook.remote = !isChanged; - } - return remoteNotebook; - } - - async add( - notebookArg: Partial< - Omit & { topics: Partial[] } - > - ) { + async add(notebookArg: Partial) { if (!notebookArg) throw new Error("Notebook cannot be undefined or null."); if (notebookArg.remote) throw new Error( @@ -121,17 +56,9 @@ export class Notebooks implements ICollection { if (oldNotebook && isTrashItem(oldNotebook)) throw new Error("Cannot modify trashed notebooks."); - if ( - !oldNotebook && - this.all.length >= 3 && - !(await checkIsUserPremium(CHECK_IDS.notebookAdd)) - ) - return; - const mergedNotebook: Partial = { ...oldNotebook, - ...notebookArg, - topics: oldNotebook?.topics || [] + ...notebookArg }; if (!mergedNotebook.title) @@ -143,7 +70,6 @@ export class Notebooks implements ICollection { title: mergedNotebook.title, description: mergedNotebook.description, pinned: !!mergedNotebook.pinned, - topics: mergedNotebook.topics || [], dateCreated: mergedNotebook.dateCreated || Date.now(), dateModified: mergedNotebook.dateModified || Date.now(), @@ -151,10 +77,6 @@ export class Notebooks implements ICollection { }; await this.collection.add(notebook); - - if (!oldNotebook && notebookArg.topics) { - await this.topics(id).add(...notebookArg.topics); - } return id; } @@ -175,7 +97,7 @@ export class Notebooks implements ICollection { get trashed() { return this.raw.filter((item) => isTrashItem(item) - ) as TrashOrItem[]; + ) as BaseTrashItem[]; } async pin(...ids: string[]) { @@ -192,18 +114,17 @@ export class Notebooks implements ICollection { } } - topics(id: string) { - return new Topics(id, this.db); - } - totalNotes(id: string) { - const notebook = this.collection.get(id); - if (!notebook || isTrashItem(notebook)) return 0; let count = 0; - for (const topic of notebook.topics) { - count += this.db.notes.topicReferences.count(topic.id); + const subNotebooks = this.db.relations.from( + { type: "notebook", id }, + "notebook" + ); + for (const notebook of subNotebooks) { + count += this.totalNotes(notebook.to.id); } - return count + this.db.relations.from(notebook, "note").resolved().length; + count += this.db.relations.from({ type: "notebook", id }, "note").length; + return count; } notebook(idOrNotebook: string | Notebook) { diff --git a/packages/core/src/collections/notes.ts b/packages/core/src/collections/notes.ts index 22f5ded5c..2d163d62a 100644 --- a/packages/core/src/collections/notes.ts +++ b/packages/core/src/collections/notes.ts @@ -20,20 +20,24 @@ along with this program. If not, see . import { createNoteModel } from "../models/note"; import { getId } from "../utils/id"; import { getContentFromData } from "../content-types"; -import { deleteItem, findById } from "../utils/array"; import { NEWLINE_STRIP_REGEX, formatTitle } from "../utils/title-format"; import { clone } from "../utils/clone"; import { Tiptap } from "../content-types/tiptap"; import { EMPTY_CONTENT, isUnencryptedContent } from "./content"; import { CHECK_IDS, checkIsUserPremium } from "../common"; import { buildFromTemplate } from "../utils/templates"; -import { Note, TrashOrItem, isTrashItem, isDeleted } from "../types"; +import { + Note, + TrashOrItem, + isTrashItem, + isDeleted, + BaseTrashItem +} from "../types"; import Database from "../api"; import { CachedCollection } from "../database/cached-collection"; import { ICollection } from "./collection"; import { NoteContent } from "./session-content"; -type NotebookReference = { id: string; topic?: string; rebuildCache?: boolean }; type ExportOptions = { format: "html" | "md" | "txt" | "md-frontmatter"; contentItem?: NoteContent; @@ -43,7 +47,6 @@ type ExportOptions = { export class Notes implements ICollection { name = "notes"; - topicReferences = new NoteIdCache(this); /** * @internal */ @@ -58,13 +61,11 @@ export class Notes implements ICollection { async init() { await this.collection.init(); - this.topicReferences.rebuild(); } async add( item: Partial; sessionId: string }> - ): Promise { - if (!item) return; + ): Promise { if (item.remote) throw new Error("Please use db.notes.merge to merge remote notes."); @@ -80,7 +81,8 @@ export class Notes implements ICollection { if (oldNote) note.contentId = oldNote.contentId; - if (!oldNote && !item.content && !item.contentId && !item.title) return; + if (!oldNote && !item.content && !item.contentId && !item.title) + throw new Error("Note must have a title or content."); if (item.content && item.content.data && item.content.type) { const { type, data } = item.content; @@ -160,7 +162,9 @@ export class Notes implements ICollection { } get trashed() { - return this.raw.filter((item) => isTrashItem(item)) as TrashOrItem[]; + return this.raw.filter((item) => + isTrashItem(item) + ) as BaseTrashItem[]; } get pinned() { @@ -289,142 +293,45 @@ export class Notes implements ICollection { if (!item) continue; const itemData = clone(item); - if (itemData.notebooks && !moveToTrash) { - for (const notebook of itemData.notebooks) { - for (const topicId of notebook.topics) { - await this.removeFromNotebook( - { id: notebook.id, topic: topicId, rebuildCache: false }, - id - ); - } - } - } - - await this.db.relations.unlinkAll(item, "tag"); - await this.db.relations.unlinkAll(item, "color"); - await this.db.relations.unlinkAll(item, "attachment"); - if (moveToTrash && !isTrashItem(itemData)) await this.db.trash.add(itemData); else { + await this.db.relations.unlinkAll(item, "tag"); + await this.db.relations.unlinkAll(item, "color"); + await this.db.relations.unlinkAll(item, "attachment"); + await this.db.relations.unlinkAll(item, "notebook"); + await this.collection.remove(id); if (itemData.contentId) await this.db.content.remove(itemData.contentId); } } - this.topicReferences.rebuild(); } - async addToNotebook(to: NotebookReference, ...noteIds: string[]) { - if (!to) throw new Error("The destination notebook cannot be undefined."); - if (!to.id) throw new Error("The destination notebook must contain id."); - - const { id: notebookId, topic: topicId } = to; - + async addToNotebook(notebookId: string, ...noteIds: string[]) { for (const noteId of noteIds) { - const note = this.collection.get(noteId); - if (!note || isTrashItem(note)) continue; - - if (topicId) { - const notebooks = note.notebooks || []; - - const noteNotebook = notebooks.find((nb) => nb.id === notebookId); - const noteHasNotebook = !!noteNotebook; - const noteHasTopic = - noteHasNotebook && noteNotebook.topics.indexOf(topicId) > -1; - if (noteHasNotebook && !noteHasTopic) { - // 1 note can be inside multiple topics - noteNotebook.topics.push(topicId); - } else if (!noteHasNotebook) { - notebooks.push({ - id: notebookId, - topics: [topicId] - }); - } - - if (!noteHasNotebook || !noteHasTopic) { - await this.db.notes.add({ - id: noteId, - notebooks - }); - this.topicReferences.add(topicId, noteId); - } - } else { - await this.db.relations.add({ id: notebookId, type: "notebook" }, note); - } + await this.db.relations.add( + { id: notebookId, type: "notebook" }, + { type: "note", id: noteId } + ); } } - async removeFromNotebook(to: NotebookReference, ...noteIds: string[]) { - if (!to) throw new Error("The destination notebook cannot be undefined."); - if (!to.id) throw new Error("The destination notebook must contain id."); - - const { id: notebookId, topic: topicId, rebuildCache = true } = to; - + async removeFromNotebook(notebookId: string, ...noteIds: string[]) { for (const noteId of noteIds) { - const note = this.collection.get(noteId); - if (!note || isTrashItem(note)) { - continue; - } - - if (topicId) { - if (!note.notebooks) continue; - const { notebooks } = note; - - const notebook = findById(notebooks, notebookId); - if (!notebook) continue; - - const { topics } = notebook; - if (!deleteItem(topics, topicId)) continue; - - if (topics.length <= 0) deleteItem(notebooks, notebook); - - await this.db.notes.add({ - id: noteId, - notebooks - }); - } else { - await this.db.relations.unlink( - { id: notebookId, type: "notebook" }, - note - ); - } + await this.db.relations.unlink( + { id: notebookId, type: "notebook" }, + { type: "note", id: noteId } + ); } - if (rebuildCache) this.topicReferences.rebuild(); } async removeFromAllNotebooks(...noteIds: string[]) { for (const noteId of noteIds) { - const note = this.collection.get(noteId); - if (!note || isTrashItem(note)) { - continue; - } - - await this.db.notes.add({ - id: noteId, - notebooks: [] - }); - await this.db.relations.unlinkAll(note, "notebook"); - } - this.topicReferences.rebuild(); - } - - /** - * @internal - */ - async _clearAllNotebookReferences(notebookId: string) { - const notes = this.db.notes.all; - - for (const note of notes) { - const { notebooks } = note; - if (!notebooks) continue; - - for (const notebook of notebooks) { - if (notebook.id !== notebookId) continue; - if (!deleteItem(notebooks, notebook)) continue; - } - - await this.collection.update(note); + await this.db.relations.unlinkAll( + { type: "note", id: noteId }, + "notebook" + ); } } @@ -453,48 +360,3 @@ export class Notes implements ICollection { function getNoteHeadline(content: Tiptap) { return content.toHeadline(); } - -class NoteIdCache { - private cache = new Map(); - constructor(private readonly notes: Notes) {} - - rebuild() { - this.cache = new Map(); - const notes = this.notes.all; - - for (const note of notes) { - const { notebooks } = note; - if (!notebooks) return; - - for (const notebook of notebooks) { - for (const topic of notebook.topics) { - this.add(topic, note.id); - } - } - } - } - - add(topicId: string, noteId: string) { - let noteIds = this.cache.get(topicId); - if (!noteIds) noteIds = []; - if (noteIds.includes(noteId)) return; - noteIds.push(noteId); - this.cache.set(topicId, noteIds); - } - - has(topicId: string, noteId: string) { - const noteIds = this.cache.get(topicId); - if (!noteIds) return false; - return noteIds.includes(noteId); - } - - count(topicId: string) { - const noteIds = this.cache.get(topicId); - if (!noteIds) return 0; - return noteIds.length; - } - - get(topicId: string) { - return this.cache.get(topicId) || []; - } -} diff --git a/packages/core/src/collections/relations.ts b/packages/core/src/collections/relations.ts index 2366e56e0..1f5fd388c 100644 --- a/packages/core/src/collections/relations.ts +++ b/packages/core/src/collections/relations.ts @@ -25,6 +25,7 @@ import Database from "../api"; type RelationsArray = Relation[] & { resolved: (limit?: number) => ItemMap[TType][]; + has: (id: string) => boolean; }; export class Relations implements ICollection { @@ -71,9 +72,18 @@ export class Relations implements ICollection { reference: ItemReference, type: TType ): RelationsArray { - const relations = this.all.filter( - (a) => compareItemReference(a.from, reference) && a.to.type === type - ); + const relations = + type === "note" || type === "notebook" + ? this.all.filter( + (a) => + compareItemReference(a.from, reference) && + a.to.type === type && + !this.db.trash.exists(a.to.id) + ) + : this.all.filter( + (a) => compareItemReference(a.from, reference) && a.to.type === type + ); + Object.defineProperties(relations, { resolved: { writable: false, @@ -81,6 +91,12 @@ export class Relations implements ICollection { configurable: false, value: (limit?: number) => this.resolve(limit ? relations.slice(0, limit) : relations, "to") + }, + has: { + writable: false, + enumerable: false, + configurable: false, + value: (id: string) => relations.some((rel) => rel.to.id === id) } }); return relations as RelationsArray; @@ -90,9 +106,17 @@ export class Relations implements ICollection { reference: ItemReference, type: TType ): RelationsArray { - const relations = this.all.filter( - (a) => compareItemReference(a.to, reference) && a.from.type === type - ); + const relations = + type === "note" || type === "notebook" + ? this.all.filter( + (a) => + compareItemReference(a.to, reference) && + a.from.type === type && + !this.db.trash.exists(a.from.id) + ) + : this.all.filter( + (a) => compareItemReference(a.to, reference) && a.from.type === type + ); Object.defineProperties(relations, { resolved: { writable: false, @@ -100,6 +124,12 @@ export class Relations implements ICollection { configurable: false, value: (limit?: number) => this.resolve(limit ? relations.slice(0, limit) : relations, "from") + }, + has: { + writable: false, + enumerable: false, + configurable: false, + value: (id: string) => relations.some((rel) => rel.from.id === id) } }); return relations as RelationsArray; @@ -133,9 +163,9 @@ export class Relations implements ICollection { await this.remove(relation.id); } - async unlinkAll(to: ItemReference, type: keyof ItemMap) { + async unlinkAll(to: ItemReference, type?: keyof ItemMap) { for (const relation of this.all.filter( - (a) => compareItemReference(a.to, to) && a.from.type === type + (a) => compareItemReference(a.to, to) && (!type || a.from.type === type) )) { await this.remove(relation.id); } diff --git a/packages/core/src/collections/shortcuts.ts b/packages/core/src/collections/shortcuts.ts index e032e71aa..d6cf7abb5 100644 --- a/packages/core/src/collections/shortcuts.ts +++ b/packages/core/src/collections/shortcuts.ts @@ -96,13 +96,6 @@ export class Shortcuts implements ICollection { item = notebook ? notebook.data : null; break; } - case "topic": { - const topic = this.db.notebooks - .topics(shortcut.item.notebookId) - .topic(id); - if (topic) item = topic._topic; - break; - } case "tag": item = this.db.tags.tag(id); break; diff --git a/packages/core/src/collections/topics.ts b/packages/core/src/collections/topics.ts deleted file mode 100644 index 8dc622985..000000000 --- a/packages/core/src/collections/topics.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* -This file is part of the Notesnook project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -import { createTopicModel } from "../models/topic"; -import { getId } from "../utils/id"; -import Database from "../api"; -import { clone } from "../utils/clone"; -import { Topic } from "../types"; - -export default class Topics { - constructor( - private readonly notebookId: string, - private readonly db: Database - ) {} - - has(topic: string) { - return this.all.findIndex((v) => v.id === topic || v.title === topic) > -1; - } - - async add(...topics: Partial[]) { - const notebook = clone(this.db.notebooks.notebook(this.notebookId)?.data); - if (!notebook) return; - const allTopics = [...notebook.topics, ...topics]; - - notebook.topics = []; - for (const t of allTopics) { - const topic = makeTopic(t, this.notebookId); - if (!topic) continue; - - if (topics.findIndex((t) => t.id === topic.id) > -1) - topic.dateEdited = Date.now(); - - const index = notebook.topics.findIndex((t) => t.id === topic.id); - if (index > -1) { - notebook.topics[index] = { - ...notebook.topics[index], - ...topic - }; - } else { - notebook.topics.push(topic); - } - } - return this.db.notebooks.collection.update(notebook); - } - - get all() { - return this.db.notebooks.notebook(this.notebookId)?.data.topics || []; - } - - topic(idOrTitleOrTopic: string | Topic) { - const topic = - typeof idOrTitleOrTopic === "string" - ? this.all.find( - (t) => t.id === idOrTitleOrTopic || t.title === idOrTitleOrTopic - ) - : idOrTitleOrTopic; - if (!topic) return; - return createTopicModel(topic, this.notebookId, this.db); - } - - async delete(...topicIds: string[]) { - const notebook = clone(this.db.notebooks.notebook(this.notebookId)?.data); - if (!notebook) return; - - const allTopics = notebook.topics; - for (const topicId of topicIds) { - const topic = this.topic(topicId); - if (!topic) continue; - - await topic.clear(); - await this.db.shortcuts.remove(topicId); - - const topicIndex = allTopics.findIndex( - (t) => t.id === topicId || t.title === topicId - ); - allTopics.splice(topicIndex, 1); - } - - return this.db.notebooks.collection.update(notebook); - } -} - -// we export this for testing. -export function makeTopic( - topic: Partial, - notebookId: string -): Topic | undefined { - if (!topic.title) return; - return { - type: "topic", - id: topic.id || getId(), - notebookId: topic.notebookId || notebookId, - title: topic.title.trim(), - dateCreated: topic.dateCreated || Date.now(), - dateEdited: topic.dateEdited || Date.now(), - dateModified: Date.now() - }; -} diff --git a/packages/core/src/collections/trash.ts b/packages/core/src/collections/trash.ts index 82fa6a378..dd78c1286 100644 --- a/packages/core/src/collections/trash.ts +++ b/packages/core/src/collections/trash.ts @@ -19,7 +19,13 @@ along with this program. If not, see . import dayjs from "dayjs"; import Database from "../api"; -import { BaseTrashItem, Note, Notebook, isTrashItem } from "../types"; +import { + BaseTrashItem, + Note, + Notebook, + TrashItem, + isTrashItem +} from "../types"; function toTrashItem(item: T): BaseTrashItem { return { @@ -33,10 +39,12 @@ function toTrashItem(item: T): BaseTrashItem { export default class Trash { collections = ["notes", "notebooks"] as const; + cache: string[] = []; constructor(private readonly db: Database) {} async init() { await this.cleanup(); + this.cache = this.all.map((t) => t.id); } async cleanup() { @@ -54,8 +62,8 @@ export default class Trash { } } - get all() { - const trashItems = []; + get all(): TrashItem[] { + const trashItems: TrashItem[] = []; for (const key of this.collections) { const collection = this.db[key]; trashItems.push(...collection.trashed); @@ -78,22 +86,22 @@ export default class Trash { } else if (item.type === "notebook") { await this.db.notebooks.collection.update(toTrashItem(item)); } + this.cache.push(item.id); } async delete(...ids: string[]) { for (const id of ids) { - if (!id) continue; const [item, collection] = this.getItem(id); if (!item || !collection) continue; if (item.itemType === "note") { if (item.contentId) await this.db.content.remove(item.contentId); await this.db.noteHistory.clearSessions(id); } else if (item.itemType === "notebook") { - await this.db.notes._clearAllNotebookReferences(item.id); + await this.db.relations.unlinkAll({ type: "notebook", id: item.id }); } await collection.remove(id); + this.cache.splice(this.cache.indexOf(id), 1); } - this.db.notes.topicReferences.rebuild(); } async restore(...ids: string[]) { @@ -108,14 +116,15 @@ export default class Trash { type: "notebook" }); } + this.cache.splice(this.cache.indexOf(id), 1); } - this.db.notes.topicReferences.rebuild(); } async clear() { for (const item of this.all) { await this.delete(item.id); } + this.cache = []; } synced(id: string) { @@ -131,6 +140,6 @@ export default class Trash { * @param {string} id */ exists(id: string) { - return this.all.findIndex((item) => item.id === id) > -1; + return this.cache.includes(id); } } diff --git a/packages/core/src/migrations.ts b/packages/core/src/migrations.ts index f26a0f1a9..8bcca168e 100644 --- a/packages/core/src/migrations.ts +++ b/packages/core/src/migrations.ts @@ -246,6 +246,15 @@ const migrations: Migration[] = [ await db.colors.delete(oldColorId); } } + + if (item.notebooks) { + for (const notebook of item.notebooks) { + for (const topic of notebook.topics) { + await db.relations.add({ type: "notebook", id: topic }, item); + } + } + } + delete item.tags; delete item.color; return true; @@ -259,6 +268,27 @@ const migrations: Migration[] = [ } delete item.noteIds; return true; + }, + notebook: async (item, db) => { + for (const topic of item.topics || []) { + const subNotebookId = await db.notebooks.add({ + id: topic.id, + title: topic.title, + dateCreated: topic.dateCreated, + dateEdited: topic.dateEdited, + dateModified: topic.dateModified + }); + if (!subNotebookId) continue; + await db.relations.add(item, { id: subNotebookId, type: "notebook" }); + } + delete item.topics; + return true; + }, + shortcut: (item) => { + if (item.item.type === "topic") { + item.item = { type: "notebook", id: item.item.id }; + return true; + } } } }, diff --git a/packages/core/src/models/notebook.ts b/packages/core/src/models/notebook.ts index 25a04fa64..e26b64305 100644 --- a/packages/core/src/models/notebook.ts +++ b/packages/core/src/models/notebook.ts @@ -18,7 +18,6 @@ along with this program. If not, see . */ import Database from "../api"; -import Topics from "../collections/topics"; import { Notebook } from "../types"; export function createNotebookModel(notebook: Notebook, db: Database) { @@ -34,10 +33,6 @@ export function createNotebookModel(notebook: Notebook, db: Database) { totalNotes: (function () { return db.notebooks.totalNotes(notebook.id); })(), - /** - * @deprecated please use `db.notebooks.topics()` instead - */ - topics: new Topics(notebook.id, db), /** * @deprecated please use `db.notebooks.pin()` & `db.notebooks.unpin()` instead. */ diff --git a/packages/core/src/models/topic.ts b/packages/core/src/models/topic.ts deleted file mode 100644 index 5456805cd..000000000 --- a/packages/core/src/models/topic.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* -This file is part of the Notesnook project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -import Database from "../api"; -import { Note, Topic } from "../types"; -import { clone } from "../utils/clone"; - -type TopicModel = Topic & { - _topic: Topic; - totalNotes: number; - has: (noteId: string) => boolean; - all: Note[]; - clear: () => Promise; -}; -export function createTopicModel( - topic: Topic, - notebookId: string, - db: Database -): TopicModel { - return Object.defineProperties(clone(topic), { - _topic: { - get: () => topic - }, - totalNotes: { - get: () => db.notes?.topicReferences.count(topic.id) - }, - has: { - value: (noteId: string) => { - return db.notes.topicReferences.has(topic.id, noteId); - } - }, - all: { - get: () => getAllNotes(db, topic.id) - }, - clear: { - value: () => { - const noteIds = db.notes?.topicReferences.get(topic.id); - if (!noteIds.length) return; - - return db.notes.removeFromNotebook( - { - topic: topic.id, - id: notebookId, - rebuildCache: true - }, - ...noteIds - ); - } - } - }) as TopicModel; -} - -function getAllNotes(db: Database, topicId: string) { - const noteIds = db.notes.topicReferences.get(topicId); - return noteIds.reduce((arr, noteId) => { - const note = db.notes.note(noteId); - if (note) arr.push(note.data); - return arr; - }, []); -} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index be4f54627..099fc4e8e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -139,6 +139,9 @@ export interface Note extends BaseItem<"note"> { * @deprecated only kept here for migration purposes. */ color?: string; + /** + * @deprecated only kept here for migration purposes. + */ notebooks?: NotebookReference[]; pinned: boolean; @@ -156,9 +159,15 @@ export interface Notebook extends BaseItem<"notebook"> { description?: string; dateEdited: number; pinned: boolean; - topics: Topic[]; + /** + * @deprecated only kept here for migration purposes. + */ + topics?: Topic[]; } +/** + * @deprecated only kept here for migration purposes. + */ export interface Topic extends BaseItem<"topic"> { title: string; notebookId: string; @@ -235,6 +244,9 @@ type TagNotebookShortcutReference = BaseShortcutReference & { type: "tag" | "notebook"; }; +/** + * @deprecated only kept here for migration purposes + */ type TopicShortcutReference = BaseShortcutReference & { type: "topic"; notebookId: string;