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;