core: get rid of noteIds in notebook topics

This is a BREAKING change in the core & will require updating the
clients. The way it works is instead of keeping noteIds of all the
notes in the topic, it keeps an in-memory cache. This in-memory
cache `topicReferences` lives in the notes collection & decouples
notes from notebooks/topics. This also simplifies all the different
actions where references would persist after the note was deleted.
Since the note acts as the source of truth for where it currently is,
there is nothing else to do except rebuild the `topicReferences`
cache.
This commit is contained in:
Abdullah Atta
2022-09-06 22:57:54 +05:00
committed by Abdullah Atta
parent ab38d89314
commit 201366b39e
14 changed files with 238 additions and 275 deletions

View File

@@ -128,38 +128,12 @@ test("merge notebook when local notebook is also edited", () =>
expect(notebook.topics.has("hello")).toBe(false); expect(notebook.topics.has("hello")).toBe(false);
})); }));
test("merge notebook when local notebook is also edited should merge noteIds too", () =>
notebookTest().then(async ({ db, id }) => {
let notebook = db.notebooks.notebook(id);
let note = await db.notes.add(TEST_NOTE);
await db.notes.move(
{ id: notebook.data.id, topic: notebook.data.topics[0].id },
note
);
const newNotebook = { ...notebook.data, remote: true };
newNotebook.topics[0].title = "hello (edited)";
newNotebook.topics[0].notes.push("hello-new-note");
await delay(500);
await notebook.topics.add({
...notebook.topics.all[0],
title: "hello (edited too)"
});
await expect(db.notebooks.merge(newNotebook)).resolves.not.toThrow();
expect(notebook.topics.all[0].notes).toHaveLength(2);
}));
test("merging notebook when local notebook is not edited should not update remote notebook dateEdited", () => test("merging notebook when local notebook is not edited should not update remote notebook dateEdited", () =>
notebookTest().then(async ({ db, id }) => { notebookTest().then(async ({ db, id }) => {
let notebook = db.notebooks.notebook(id); let notebook = db.notebooks.notebook(id);
let note = await db.notes.add(TEST_NOTE); let note = await db.notes.add(TEST_NOTE);
await db.notes.move( await db.notes.addToNotebook(
{ id: notebook.data.id, topic: notebook.data.topics[0].id }, { id: notebook.data.id, topic: notebook.data.topics[0].id },
note note
); );

View File

@@ -60,7 +60,9 @@ test("delete note", () =>
let notebookId = await db.notebooks.add(TEST_NOTEBOOK); let notebookId = await db.notebooks.add(TEST_NOTEBOOK);
let topics = db.notebooks.notebook(notebookId).topics; let topics = db.notebooks.notebook(notebookId).topics;
let topic = topics.topic("hello"); let topic = topics.topic("hello");
await topic.add(id);
await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
topic = topics.topic("hello"); topic = topics.topic("hello");
expect(topic.all.findIndex((v) => v.id === id)).toBeGreaterThan(-1); expect(topic.all.findIndex((v) => v.id === id)).toBeGreaterThan(-1);
@@ -192,7 +194,9 @@ test("add note to topic", () =>
let topics = db.notebooks.notebook(notebookId).topics; let topics = db.notebooks.notebook(notebookId).topics;
await topics.add("Home"); await topics.add("Home");
let topic = topics.topic("Home"); let topic = topics.topic("Home");
await topic.add(id);
await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
topic = topics.topic("Home"); topic = topics.topic("Home");
expect(topic.all).toHaveLength(1); expect(topic.all).toHaveLength(1);
expect(topic.totalNotes).toBe(1); expect(topic.totalNotes).toBe(1);
@@ -207,7 +211,9 @@ test("duplicate note to topic should not be added", () =>
let topics = db.notebooks.notebook(notebookId).topics; let topics = db.notebooks.notebook(notebookId).topics;
await topics.add("Home"); await topics.add("Home");
let topic = topics.topic("Home"); let topic = topics.topic("Home");
await topic.add(id);
await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
topic = topics.topic("Home"); topic = topics.topic("Home");
expect(topic.all).toHaveLength(1); expect(topic.all).toHaveLength(1);
})); }));
@@ -218,7 +224,7 @@ test("add the same note to 2 notebooks", () =>
let topics = db.notebooks.notebook(notebookId).topics; let topics = db.notebooks.notebook(notebookId).topics;
await topics.add("Home"); await topics.add("Home");
let topic = topics.topic("Home")._topic; let topic = topics.topic("Home")._topic;
await db.notes.move({ id: notebookId, topic: topic.id }, id); await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
expect(topics.topic(topic.id).has(id)).toBe(true); expect(topics.topic(topic.id).has(id)).toBe(true);
@@ -226,7 +232,7 @@ test("add the same note to 2 notebooks", () =>
let topics2 = db.notebooks.notebook(notebookId2).topics; let topics2 = db.notebooks.notebook(notebookId2).topics;
await topics2.add("Home2"); await topics2.add("Home2");
let topic2 = topics2.topic("Home2")._topic; let topic2 = topics2.topic("Home2")._topic;
await db.notes.move({ id: notebookId2, topic: topic2.id }, id); await db.notes.addToNotebook({ id: notebookId2, topic: topic2.id }, id);
let note = db.notes.note(id); let note = db.notes.note(id);
expect(note.notebooks).toHaveLength(2); expect(note.notebooks).toHaveLength(2);
@@ -239,8 +245,10 @@ test("moving note to same notebook and topic should do nothing", () =>
let topics = db.notebooks.notebook(notebookId).topics; let topics = db.notebooks.notebook(notebookId).topics;
await topics.add("Home"); await topics.add("Home");
let topic = topics.topic("Home"); let topic = topics.topic("Home");
await topic.add(id);
await db.notes.move({ id: notebookId, topic: "Home" }, id); await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
let note = db.notes.note(id); let note = db.notes.note(id);
expect(note.notebooks.some((n) => n.id === notebookId)).toBe(true); expect(note.notebooks.some((n) => n.id === notebookId)).toBe(true);
})); }));
@@ -343,16 +351,6 @@ test("note content should not contain image base64 data after save", () =>
expect(content).not.toContain(`src=`); expect(content).not.toContain(`src=`);
})); }));
test("repairing notebook references should delete non-existent notebooks", () =>
noteTest({
...TEST_NOTE,
notebooks: [{ id: "hello", topics: ["helloworld"] }]
}).then(async ({ db, id }) => {
await db.notes.repairReferences();
let note = db.notes.note(id);
expect(note.notebooks).toHaveLength(0);
}));
test("adding a note with an invalid tag should clean the tag array", () => test("adding a note with an invalid tag should clean the tag array", () =>
databaseTest().then(async (db) => { databaseTest().then(async (db) => {
await expect( await expect(

View File

@@ -51,25 +51,25 @@ test("add note to topic", () =>
let topics = db.notebooks.notebook(id).topics; let topics = db.notebooks.notebook(id).topics;
let topic = topics.topic("hello"); let topic = topics.topic("hello");
let noteId = await db.notes.add(TEST_NOTE); let noteId = await db.notes.add(TEST_NOTE);
await topic.add(noteId); await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
topic = topics.topic("hello"); topic = topics.topic("hello");
expect(topic.totalNotes).toBe(1); expect(topic.totalNotes).toBe(1);
expect(db.notebooks.notebook(id).totalNotes).toBe(1); expect(db.notebooks.notebook(id).totalNotes).toBe(1);
})); }));
test("delete note to topic", () => test("delete note of a topic", () =>
notebookTest().then(async ({ db, id }) => { notebookTest().then(async ({ db, id }) => {
let topics = db.notebooks.notebook(id).topics; let topics = db.notebooks.notebook(id).topics;
let topic = topics.topic("hello"); let topic = topics.topic("hello");
let noteId = await db.notes.add(TEST_NOTE); let noteId = await db.notes.add(TEST_NOTE);
await topic.add(noteId); await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
topic = topics.topic("hello"); topic = topics.topic("hello");
expect(topic.totalNotes).toBe(1); expect(topic.totalNotes).toBe(1);
expect(db.notebooks.notebook(id).totalNotes).toBe(1); expect(db.notebooks.notebook(id).totalNotes).toBe(1);
await topic.delete(noteId); await db.notes.removeFromNotebook({ id, topic: topic.id }, noteId);
topic = topics.topic("hello"); topic = topics.topic("hello");
expect(topic.totalNotes).toBe(0); expect(topic.totalNotes).toBe(0);
@@ -116,7 +116,8 @@ test("get topic", () =>
let noteId = await db.notes.add({ let noteId = await db.notes.add({
content: TEST_NOTE.content content: TEST_NOTE.content
}); });
await topic.add(noteId); await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
topic = topics.topic("Home"); topic = topics.topic("Home");
expect(await db.content.get(topic.all[0].contentId)).toBeDefined(); expect(await db.content.get(topic.all[0].contentId)).toBeDefined();
expect(topic.totalNotes).toBe(1); expect(topic.totalNotes).toBe(1);
@@ -136,7 +137,7 @@ test("delete note from edited topic", () =>
let topics = db.notebooks.notebook(id).topics; let topics = db.notebooks.notebook(id).topics;
await topics.add("Home"); await topics.add("Home");
let topic = topics.topic("Home"); let topic = topics.topic("Home");
await db.notes.move({ id, topic: topic._topic.title }, noteId); await db.notes.addToNotebook({ id, topic: topic._topic.title }, noteId);
await topics.add({ id: topic._topic.id, title: "Hello22" }); await topics.add({ id: topic._topic.id, title: "Hello22" });
await db.notes.delete(noteId); await db.notes.delete(noteId);
}) })

View File

@@ -57,7 +57,9 @@ test("permanently delete a note", () =>
test("restore a deleted note that was in a notebook", () => test("restore a deleted note that was in a notebook", () =>
noteTest().then(async ({ db, id }) => { noteTest().then(async ({ db, id }) => {
let nbId = await db.notebooks.add(TEST_NOTEBOOK); let nbId = await db.notebooks.add(TEST_NOTEBOOK);
await db.notebooks.notebook(nbId).topics.topic("hello").add(id); const topic = db.notebooks.notebook(nbId).topics.topic("hello");
await db.notes.addToNotebook({ id: nbId, topic: topic.id }, id);
await db.notes.delete(id); await db.notes.delete(id);
await db.trash.restore(db.trash.all[0].id); await db.trash.restore(db.trash.all[0].id);
expect(db.trash.all).toHaveLength(0); expect(db.trash.all).toHaveLength(0);
@@ -68,7 +70,7 @@ test("restore a deleted note that was in a notebook", () =>
expect(await note.content()).toBe(TEST_NOTE.content.data); expect(await note.content()).toBe(TEST_NOTE.content.data);
const notebook = db.notebooks.notebook(nbId); const notebook = db.notebooks.notebook(nbId);
expect(notebook.topics.topic("hello").has(id)).toBe(true); expect(notebook.topics.topic(topic.id).has(id)).toBe(true);
expect(note.notebooks.some((n) => n.id === nbId)).toBe(true); expect(note.notebooks.some((n) => n.id === nbId)).toBe(true);
@@ -102,7 +104,9 @@ test("restore a deleted locked note", () =>
test("restore a deleted note that's in a deleted notebook", () => test("restore a deleted note that's in a deleted notebook", () =>
noteTest().then(async ({ db, id }) => { noteTest().then(async ({ db, id }) => {
let nbId = await db.notebooks.add(TEST_NOTEBOOK); let nbId = await db.notebooks.add(TEST_NOTEBOOK);
await db.notebooks.notebook(nbId).topics.topic("hello").add(id); const topic = db.notebooks.notebook(nbId).topics.topic("hello");
await db.notes.addToNotebook({ id: nbId, topic: topic.id }, id);
await db.notes.delete(id); await db.notes.delete(id);
await db.notebooks.delete(nbId); await db.notebooks.delete(nbId);
const deletedNote = db.trash.all.find( const deletedNote = db.trash.all.find(
@@ -117,7 +121,10 @@ test("restore a deleted note that's in a deleted notebook", () =>
test("delete a notebook", () => test("delete a notebook", () =>
notebookTest().then(async ({ db, id }) => { notebookTest().then(async ({ db, id }) => {
let noteId = await db.notes.add(TEST_NOTE); let noteId = await db.notes.add(TEST_NOTE);
await db.notebooks.notebook(id).topics.topic("hello").add(noteId); let notebook = db.notebooks.notebook(id);
const topic = notebook.topics.topic("hello");
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
await db.notebooks.delete(id); await db.notebooks.delete(id);
expect(db.notebooks.notebook(id)).toBeUndefined(); expect(db.notebooks.notebook(id)).toBeUndefined();
expect(db.notes.note(noteId).notebook).toBeUndefined(); expect(db.notes.note(noteId).notebook).toBeUndefined();
@@ -126,7 +133,9 @@ test("delete a notebook", () =>
test("restore a deleted notebook", () => test("restore a deleted notebook", () =>
notebookTest().then(async ({ db, id }) => { notebookTest().then(async ({ db, id }) => {
let noteId = await db.notes.add(TEST_NOTE); let noteId = await db.notes.add(TEST_NOTE);
await db.notebooks.notebook(id).topics.topic("hello").add(noteId); const topic = db.notebooks.notebook(id).topics.topic("hello");
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
await db.notebooks.delete(id); await db.notebooks.delete(id);
await db.trash.restore(id); await db.trash.restore(id);
@@ -137,21 +146,24 @@ test("restore a deleted notebook", () =>
const noteNotebook = note.notebooks.find((n) => n.id === id); const noteNotebook = note.notebooks.find((n) => n.id === id);
expect(noteNotebook).toBeDefined(); expect(noteNotebook).toBeDefined();
expect(noteNotebook.topics).toHaveLength(1); expect(noteNotebook.topics).toHaveLength(1);
expect(notebook.topics.topic(noteNotebook.topics[0])).toBeDefined(); expect(notebook.topics.topic(noteNotebook.topics[0])).toBeDefined();
})); }));
test("restore a notebook that has deleted notes", () => test("restore a notebook that has deleted notes", () =>
notebookTest().then(async ({ db, id }) => { notebookTest().then(async ({ db, id }) => {
let noteId = await db.notes.add(TEST_NOTE); let noteId = await db.notes.add(TEST_NOTE);
await db.notebooks.notebook(id).topics.topic("hello").add(noteId);
let notebook = db.notebooks.notebook(id);
const topic = notebook.topics.topic("hello");
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
await db.notebooks.delete(id); await db.notebooks.delete(id);
await db.notes.delete(noteId); await db.notes.delete(noteId);
const deletedNotebook = db.trash.all.find( const deletedNotebook = db.trash.all.find(
(v) => v.id === id && v.itemType === "notebook" (v) => v.id === id && v.itemType === "notebook"
); );
await db.trash.restore(deletedNotebook.id); await db.trash.restore(deletedNotebook.id);
let notebook = db.notebooks.notebook(id); notebook = db.notebooks.notebook(id);
expect(notebook).toBeDefined(); expect(notebook).toBeDefined();
expect(notebook.topics.topic("hello").has(noteId)).toBe(false); expect(notebook.topics.topic("hello").has(noteId)).toBe(false);
})); }));

View File

@@ -4,10 +4,10 @@ exports[`strip note with content: stripped-note-with-content 1`] = `"{\\"title\\
exports[`strip note: stripped-note 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":true,\\"colored\\":false,\\"type\\":\\"note\\",\\"tags\\":[],\\"id\\":\\"hello\\",\\"contentId\\":\\"hello2\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`; exports[`strip note: stripped-note 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":true,\\"colored\\":false,\\"type\\":\\"note\\",\\"tags\\":[],\\"id\\":\\"hello\\",\\"contentId\\":\\"hello2\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`;
exports[`strip notebook: stripped-notebook 1`] = `"{\\"title\\":true,\\"description\\":true,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"notebook\\",\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123,\\"additionalData\\":[{\\"type\\":\\"topic\\",\\"id\\":\\"hello\\",\\"notebookId\\":\\"hello23\\",\\"title\\":\\"hello\\",\\"dateCreated\\":123,\\"dateEdited\\":123,\\"notes\\":[],\\"dateModified\\":123}]}"`; exports[`strip notebook: stripped-notebook 1`] = `"{\\"title\\":true,\\"description\\":true,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"notebook\\",\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123,\\"additionalData\\":[{\\"type\\":\\"topic\\",\\"id\\":\\"hello\\",\\"notebookId\\":\\"hello23\\",\\"title\\":\\"hello\\",\\"dateCreated\\":123,\\"dateEdited\\":123,\\"dateModified\\":123}]}"`;
exports[`strip tag: stripped-tag 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"tag\\",\\"noteIds\\":[],\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`; exports[`strip tag: stripped-tag 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"tag\\",\\"noteIds\\":[],\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`;
exports[`strip topic: stripped-topic 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"topic\\",\\"notes\\":[],\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`; exports[`strip topic: stripped-topic 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"topic\\",\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`;
exports[`strip trashed note: stripped-trashed-note 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":true,\\"colored\\":false,\\"type\\":\\"trash\\",\\"tags\\":[],\\"id\\":\\"hello\\",\\"contentId\\":\\"hello2\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateDeleted\\":123,\\"dateCreated\\":123}"`; exports[`strip trashed note: stripped-trashed-note 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":true,\\"colored\\":false,\\"type\\":\\"trash\\",\\"tags\\":[],\\"id\\":\\"hello\\",\\"contentId\\":\\"hello2\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateDeleted\\":123,\\"dateCreated\\":123}"`;

View File

@@ -296,7 +296,7 @@ test("issue: remove notebook reference from notes that are removed from topic du
expect(deviceB.notebooks.notebook(id)).toBeDefined(); expect(deviceB.notebooks.notebook(id)).toBeDefined();
const noteA = await deviceA.notes.add({ title: "Note 1" }); const noteA = await deviceA.notes.add({ title: "Note 1" });
await deviceA.notes.move({ id, topic: "Topic 1" }, noteA); await deviceA.notes.addToNotebook({ id, topic: "Topic 1" }, noteA);
expect( expect(
deviceA.notebooks.notebook(id).topics.topic("Topic 1").totalNotes deviceA.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
@@ -305,7 +305,7 @@ test("issue: remove notebook reference from notes that are removed from topic du
await delay(2000); await delay(2000);
const noteB = await deviceB.notes.add({ title: "Note 2" }); const noteB = await deviceB.notes.add({ title: "Note 2" });
await deviceB.notes.move({ id, topic: "Topic 1" }, noteB); await deviceB.notes.addToNotebook({ id, topic: "Topic 1" }, noteB);
expect( expect(
deviceB.notebooks.notebook(id).topics.topic("Topic 1").totalNotes deviceB.notebooks.notebook(id).topics.topic("Topic 1").totalNotes

View File

@@ -371,6 +371,8 @@ class Sync {
async onRemoteSyncCompleted(lastSynced) { async onRemoteSyncCompleted(lastSynced) {
// refresh monographs on sync completed // refresh monographs on sync completed
await this.db.monographs.init(); await this.db.monographs.init();
// refresh topic references
this.db.notes.topicReferences.refresh();
await this.start(false, false, lastSynced); await this.start(false, false, lastSynced);
} }

View File

@@ -22,7 +22,6 @@ import Notebook from "../models/notebook";
import getId from "../utils/id"; import getId from "../utils/id";
import { CHECK_IDS, checkIsUserPremium } from "../common"; import { CHECK_IDS, checkIsUserPremium } from "../common";
import { qclone } from "qclone"; import { qclone } from "qclone";
import setManipulator from "../utils/set";
export default class Notebooks extends Collection { export default class Notebooks extends Collection {
async merge(remoteNotebook) { async merge(remoteNotebook) {
@@ -36,7 +35,6 @@ export default class Notebooks extends Collection {
const lastSyncedTimestamp = await this._db.lastSynced(); const lastSyncedTimestamp = await this._db.lastSynced();
let isChanged = false; let isChanged = false;
// merge new and old topics // merge new and old topics
// We need to handle 3 cases:
for (let oldTopic of localNotebook.topics) { for (let oldTopic of localNotebook.topics) {
const newTopicIndex = remoteNotebook.topics.findIndex( const newTopicIndex = remoteNotebook.topics.findIndex(
(t) => t.id === oldTopic.id (t) => t.id === oldTopic.id
@@ -60,25 +58,10 @@ export default class Notebooks extends Collection {
else if (newTopic && oldTopic.dateEdited > newTopic.dateEdited) { else if (newTopic && oldTopic.dateEdited > newTopic.dateEdited) {
remoteNotebook.topics[newTopicIndex] = { remoteNotebook.topics[newTopicIndex] = {
...oldTopic, ...oldTopic,
notes: setManipulator.union(oldTopic.notes, newTopic.notes),
dateEdited: Date.now() dateEdited: Date.now()
}; };
isChanged = true; isChanged = true;
} }
// CASE 4: if topic exists in both notebooks:
// if newTopic.dateEdited > oldTopic.dateEdited: we iterate
// on all notes that are not in newTopic (if any)
// and dereference them.
else if (newTopic && newTopic.dateEdited > oldTopic.dateEdited) {
const removedNotes = setManipulator.complement(
oldTopic.notes,
newTopic.notes
);
await this.notebook(remoteNotebook.id)
.topics.topic(oldTopic.id)
.delete(...removedNotes);
}
} }
remoteNotebook.remote = !isChanged; remoteNotebook.remote = !isChanged;
} }
@@ -163,20 +146,10 @@ export default class Notebooks extends Collection {
let notebook = this.notebook(id); let notebook = this.notebook(id);
if (!notebook) continue; if (!notebook) continue;
const notebookData = qclone(notebook.data); const notebookData = qclone(notebook.data);
await notebook.topics.delete(...notebook.data.topics); // await notebook.topics.delete(...notebook.data.topics);
await this._collection.removeItem(id); await this._collection.removeItem(id);
await this._db.settings.unpin(id); await this._db.settings.unpin(id);
await this._db.trash.add(notebookData); await this._db.trash.add(notebookData);
} }
} }
async repairReferences() {
for (let notebook of this.all) {
const _notebook = this.notebook(notebook);
for (let topic of notebook.topics) {
const _topic = _notebook.topics.topic(topic.id);
await _topic.add(...topic.notes);
}
}
}
} }

View File

@@ -22,9 +22,23 @@ import Note from "../models/note";
import getId from "../utils/id"; import getId from "../utils/id";
import { getContentFromData } from "../content-types"; import { getContentFromData } from "../content-types";
import qclone from "qclone/src/qclone"; import qclone from "qclone/src/qclone";
import { deleteItem } from "../utils/array"; import { deleteItem, findById } from "../utils/array";
/**
* @typedef {{ id: string, topic: string, rebuildCache?: boolean }} NotebookReference
*/
export default class Notes extends Collection { export default class Notes extends Collection {
constructor(db, name, cached) {
super(db, name, cached);
this.topicReferences = new NoteIdCache(this._db);
}
async init() {
await super.init();
this.topicReferences.rebuild();
}
async merge(remoteNote) { async merge(remoteNote) {
if (!remoteNote) return; if (!remoteNote) return;
@@ -119,7 +133,6 @@ export default class Notes extends Collection {
await this._collection.addItem(note); await this._collection.addItem(note);
await this._resolveColorAndTags(note); await this._resolveColorAndTags(note);
await this._resolveNotebooks(note);
return note.id; return note.id;
} }
@@ -207,16 +220,13 @@ export default class Notes extends Collection {
if (!item) continue; if (!item) continue;
const itemData = qclone(item.data); const itemData = qclone(item.data);
if (itemData.notebooks) { if (itemData.notebooks && !moveToTrash) {
for (let notebook of itemData.notebooks) { for (let notebook of itemData.notebooks) {
const notebookRef = this._db.notebooks.notebook(notebook.id);
if (!notebookRef) continue;
for (let topicId of notebook.topics) { for (let topicId of notebook.topics) {
const topic = notebookRef.topics.topic(topicId); await this.removeFromNotebook(
if (!topic) continue; { id: notebook.id, topic: topicId, rebuildCache: false },
id
await topic.delete(id); );
} }
} }
} }
@@ -244,46 +254,7 @@ export default class Notes extends Collection {
await this._db.content.remove(itemData.contentId); await this._db.content.remove(itemData.contentId);
} }
} }
} this.topicReferences.rebuild();
async move(to, ...noteIds) {
if (!to) throw new Error("The destination notebook cannot be undefined.");
if (!to.id || !to.topic)
throw new Error(
"The destination notebook must contain notebookId and topic."
);
let topic = this._db.notebooks.notebook(to.id).topics.topic(to.topic);
if (!topic) throw new Error("No such topic exists.");
await topic.add(...noteIds);
}
async repairReferences(notes) {
notes = notes || this.all;
for (let note of notes) {
const { notebooks } = note;
if (!notebooks) continue;
for (let notebook of notebooks) {
const nb = this._db.notebooks.notebook(notebook.id);
if (nb) {
for (let topic of notebook.topics) {
const _topic = nb.topics.topic(topic);
if (!_topic || !_topic.has(note.id)) {
deleteItem(notebook.topics, topic);
await this.add(note);
continue;
}
}
}
if (!nb || !notebook.topics.length) {
deleteItem(notebooks, notebook);
await this.add(note);
continue;
}
}
}
} }
async _resolveColorAndTags(note) { async _resolveColorAndTags(note) {
@@ -307,16 +278,97 @@ export default class Notes extends Collection {
} }
} }
async _resolveNotebooks(note) { /**
const { notebooks, id } = note; * @param {NotebookReference} to
if (!notebooks) return; */
async addToNotebook(to, ...noteIds) {
if (!to) throw new Error("The destination notebook cannot be undefined.");
if (!to.id || !to.topic)
throw new Error(
"The destination notebook must contain notebookId and topic."
);
for (const notebook of notebooks) { const { id: notebookId, topic: topicId } = to;
const nb = this._db.notebooks.notebook(notebook.id);
if (!nb) continue; for (let noteId of noteIds) {
for (const topic of notebook.topics) { let note = this._db.notes.note(noteId);
await this.move({ id: notebook.id, topic }, id); if (!note || note.data.deleted) continue;
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);
}
}
}
/**
* @param {NotebookReference} to
*/
async removeFromNotebook(to, ...noteIds) {
if (!to) throw new Error("The destination notebook cannot be undefined.");
if (!to.id || !to.topic)
throw new Error(
"The destination notebook must contain notebookId and topic."
);
const { id: notebookId, topic: topicId, rebuildCache = true } = to;
for (const noteId of noteIds) {
const note = this.note(noteId);
if (!note || note.deleted || !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
});
}
if (rebuildCache) this.topicReferences.rebuild();
}
async _clearAllNotebookReferences(notebookId) {
const notes = this._db.notes.all;
for (const note of notes) {
const { notebooks } = note;
if (!notebooks) continue;
for (let notebook of notebooks) {
if (notebook.id !== notebookId) continue;
if (!deleteItem(notebooks, notebook)) continue;
}
await this._collection.updateItem(note);
} }
} }
} }
@@ -339,3 +391,54 @@ function getNoteTitle(note, oldNote) {
timeStyle: "short" timeStyle: "short"
})}`; })}`;
} }
class NoteIdCache {
/**
*
* @param {import("../api/index").default} db
*/
constructor(db) {
this._db = db;
this.cache = new Map();
}
rebuild() {
this.cache = new Map();
const notes = this._db.notes.all;
for (const note of notes) {
const { notebooks } = note;
if (!notebooks) continue;
for (let notebook of notebooks) {
for (let topic of notebook.topics) {
this.add(topic, note.id);
}
}
}
}
add(topicId, noteId) {
let noteIds = this.cache.get(topicId);
if (!noteIds) noteIds = [];
if (noteIds.includes(noteId)) return;
noteIds.push(noteId);
this.cache.set(topicId, noteIds);
}
has(topicId, noteId) {
let noteIds = this.cache.get(topicId);
if (!noteIds) return false;
return noteIds.includes(noteId);
}
count(topicId) {
let noteIds = this.cache.get(topicId);
if (!noteIds) return 0;
return noteIds.length;
}
get(topicId) {
return this.cache.get(topicId) || [];
}
}

View File

@@ -116,7 +116,7 @@ export default class Topics {
const topic = this.topic(topicId); const topic = this.topic(topicId);
if (!topic) continue; if (!topic) continue;
await topic.delete(...topic._topic.notes); await topic.clear();
await this._db.settings.unpin(topicId); await this._db.settings.unpin(topicId);
const topicIndex = allTopics.findIndex( const topicIndex = allTopics.findIndex(
@@ -138,7 +138,6 @@ export function makeTopic(topic, notebookId) {
notebookId, notebookId,
title: topic.trim(), title: topic.trim(),
dateCreated: Date.now(), dateCreated: Date.now(),
dateEdited: Date.now(), dateEdited: Date.now()
notes: []
}; };
} }

View File

@@ -83,9 +83,12 @@ export default class Trash {
if (item.itemType === "note") { if (item.itemType === "note") {
await this._db.content.remove(item.contentId); await this._db.content.remove(item.contentId);
await this._db.noteHistory.clearSessions(id); await this._db.noteHistory.clearSessions(id);
} else if (item.itemType === "notebook") {
await this._db.notes._clearAllNotebookReferences(item.id);
} }
await collection.removeItem(id); await collection.removeItem(id);
} }
this._db.notes.topicReferences.rebuild();
} }
async restore(...ids) { async restore(...ids) {
@@ -99,44 +102,12 @@ export default class Trash {
delete item.itemType; delete item.itemType;
if (item.type === "note") { if (item.type === "note") {
let { notebooks } = item;
item.notebooks = undefined;
await this.collections.notes.add(item); await this.collections.notes.add(item);
if (notebooks) {
for (let nb of notebooks) {
const { id, topics } = nb;
for (let topic of topics) {
// if the notebook or topic has been deleted
if (
!this._db.notebooks._collection.exists(id) ||
!this._db.notebooks.notebook(id).topics.has(topic)
) {
notebooks = undefined;
continue;
}
// restore the note to the topic it was in before deletion
await this._db.notebooks
.notebook(id)
.topics.topic(topic)
.add(item.id);
}
}
}
} else if (item.type === "notebook") { } else if (item.type === "notebook") {
const { topics } = item;
item.topics = [];
await this.collections.notebooks.add(item); await this.collections.notebooks.add(item);
let notebook = this._db.notebooks.notebook(item.id);
for (let topic of topics) {
await notebook.topics.add(topic.title);
let t = notebook.topics.topic(topic.title);
if (!t) continue;
if (topic.notes) await t.add(...topic.notes);
}
} }
} }
this._db.notes.topicReferences.rebuild();
} }
async clear() { async clear() {

View File

@@ -181,12 +181,7 @@ export default class Backup {
]; ];
await this._db.syncer.acquireLock(async () => { await this._db.syncer.acquireLock(async () => {
if ( await this._migrator.migrate(collections, (id) => data[id], version);
await this._migrator.migrate(collections, (id) => data[id], version)
) {
await this._db.notes.repairReferences();
await this._db.notebooks.repairReferences();
}
}); });
} }

View File

@@ -32,7 +32,7 @@ export default class Notebook {
get totalNotes() { get totalNotes() {
return this._notebook.topics.reduce((sum, topic) => { return this._notebook.topics.reduce((sum, topic) => {
return sum + topic.notes.length; return sum + this._db.notes.topicReferences.count(topic.id);
}, 0); }, 0);
} }

View File

@@ -17,9 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { qclone } from "qclone";
import { deleteItem, findById } from "../utils/array";
export default class Topic { export default class Topic {
/** /**
* @param {Object} topic * @param {Object} topic
@@ -33,98 +30,36 @@ export default class Topic {
} }
get totalNotes() { get totalNotes() {
return this._topic.notes.length; return this._db.notes.topicReferences.count(this.id);
}
get id() {
return this._topic.id;
} }
has(noteId) { has(noteId) {
return this._topic.notes.indexOf(noteId) > -1; return this._db.notes.topicReferences.has(this.id, noteId);
}
async add(...noteIds) {
const topic = qclone(this._topic);
for (let noteId of noteIds) {
let note = this._db.notes.note(noteId);
if (!note || note.data.deleted) continue;
const notebooks = note.notebooks || [];
const noteNotebook = notebooks.find((nb) => nb.id === this._notebookId);
const noteHasNotebook = !!noteNotebook;
const noteHasTopic =
noteHasNotebook && noteNotebook.topics.indexOf(topic.id) > -1;
if (noteHasNotebook && !noteHasTopic) {
// 1 note can be inside multiple topics
noteNotebook.topics.push(topic.id);
} else if (!noteHasNotebook) {
notebooks.push({
id: this._notebookId,
topics: [topic.id]
});
}
if (!noteHasNotebook || !noteHasTopic) {
await this._db.notes.add({
id: noteId,
notebooks
});
}
if (!this.has(noteId)) {
topic.notes.push(noteId);
await this._save(topic);
}
}
}
async delete(...noteIds) {
const topic = qclone(this._topic);
for (let noteId of noteIds) {
let note = this._db.notes.note(noteId);
if (
!note ||
note.deleted ||
!deleteItem(topic.notes, noteId) ||
!note.notebooks
) {
continue;
}
let { notebooks } = note;
const notebook = findById(notebooks, this._notebookId);
if (!notebook) continue;
const { topics } = notebook;
if (!deleteItem(topics, topic.id)) continue;
if (topics.length <= 0) deleteItem(notebooks, notebook);
await this._db.notes.add({
id: noteId,
notebooks
});
}
return await this._save(topic);
}
async _save(topic) {
await this._db.notebooks.notebook(this._notebookId).topics.add(topic);
return this;
} }
get all() { get all() {
return this._topic.notes.reduce((arr, noteId) => { const noteIds = this._db.notes.topicReferences.get(this.id);
if (!noteIds.length) return [];
return noteIds.reduce((arr, noteId) => {
let note = this._db.notes.note(noteId); let note = this._db.notes.note(noteId);
if (note) arr.push(note.data); if (note) arr.push(note.data);
return arr; return arr;
}, []); }, []);
} }
synced() { clear() {
const notes = this._topic.notes; const noteIds = this._db.notes.topicReferences.get(this.id);
for (let id of notes) { if (!noteIds.length) return;
if (!this._db.notes.exists(id)) return false;
} return this._db.notes.deleteFromNotebook(
return true; this._notebookId,
this.id,
...noteIds
);
} }
} }