From 1b53f967d8da34275eacdb417cf82ee6de1fe163 Mon Sep 17 00:00:00 2001 From: thecodrr Date: Thu, 6 Feb 2020 16:46:23 +0500 Subject: [PATCH] feat: implement trash --- packages/core/__tests__/notebooks.test.js | 6 ++ packages/core/__tests__/trash.test.js | 103 ++++++++++++++++++++ packages/core/api/index.js | 7 +- packages/core/collections/notebooks.js | 18 +++- packages/core/collections/notes.js | 7 +- packages/core/collections/topics.js | 25 ++--- packages/core/collections/trash.js | 93 ++++++++++++++++++ packages/core/database/cached-collection.js | 4 + 8 files changed, 242 insertions(+), 21 deletions(-) create mode 100644 packages/core/__tests__/trash.test.js create mode 100644 packages/core/collections/trash.js diff --git a/packages/core/__tests__/notebooks.test.js b/packages/core/__tests__/notebooks.test.js index 6d1ef3f7a..1d64cc7de 100644 --- a/packages/core/__tests__/notebooks.test.js +++ b/packages/core/__tests__/notebooks.test.js @@ -71,6 +71,12 @@ test("delete a notebook", () => .notebook(id) .topics.topic("General") .add(noteId); + expect( + db.notebooks + .notebook(id) + .topics.topic("General") + .has(noteId) + ).toBe(true); let note = db.notes.note(noteId); expect(note.notebook.id).toBe(id); await db.notebooks.delete(id); diff --git a/packages/core/__tests__/trash.test.js b/packages/core/__tests__/trash.test.js new file mode 100644 index 000000000..8a0833eed --- /dev/null +++ b/packages/core/__tests__/trash.test.js @@ -0,0 +1,103 @@ +import { + StorageInterface, + noteTest, + notebookTest, + TEST_NOTE, + TEST_NOTEBOOK +} from "./utils"; + +beforeEach(() => StorageInterface.clear()); + +test("delete a note", () => + noteTest().then(async ({ db, id }) => { + let nbId = await db.notebooks.add(TEST_NOTEBOOK); + await db.notebooks + .notebook(nbId) + .topics.topic("General") + .add(id); + await db.notes.delete(id); + expect(db.trash.all.length).toBe(1); + expect(await db.trash.deltaStorage.read(id + "_delta")).toBeDefined(); + await db.trash.delete(db.trash.all[0].id); + expect(db.trash.all.length).toBe(0); + expect(await db.trash.deltaStorage.read(id + "_delta")).toBeUndefined(); + })); + +test("restore a deleted note", () => + noteTest().then(async ({ db, id }) => { + let nbId = await db.notebooks.add(TEST_NOTEBOOK); + await db.notebooks + .notebook(nbId) + .topics.topic("General") + .add(id); + await db.notes.delete(id); + await db.trash.restore(db.trash.all[0].id); + expect(db.trash.all.length).toBe(0); + let note = db.notes.note(id); + expect(note).toBeDefined(); + expect( + db.notebooks + .notebook(nbId) + .topics.topic("General") + .has(id) + ).toBe(true); + expect(db.notes.note(id).notebook.id).toBe(nbId); + expect(db.notes.note(id).notebook.topic).toBe("General"); + })); + +test("restore a deleted note that's in a deleted notebook", () => + noteTest().then(async ({ db, id }) => { + let nbId = await db.notebooks.add(TEST_NOTEBOOK); + await db.notebooks + .notebook(nbId) + .topics.topic("General") + .add(id); + await db.notes.delete(id); + await db.notebooks.delete(nbId); + await db.trash.restore(db.trash.all[0].id); + let note = db.notes.note(id); + expect(note).toBeDefined(); + expect(db.notes.note(id).notebook).toStrictEqual({}); + })); + +test("delete a notebook", () => + notebookTest().then(async ({ db, id }) => { + let noteId = await db.notes.add(TEST_NOTE); + await db.notebooks + .notebook(id) + .topics.topic("General") + .add(noteId); + await db.notebooks.delete(id); + expect(db.notebooks.notebook(id)).toBeUndefined(); + expect(db.notes.note(noteId).notebook).toStrictEqual({}); + })); + +test("restore a deleted notebook", () => + notebookTest().then(async ({ db, id }) => { + let noteId = await db.notes.add(TEST_NOTE); + await db.notebooks + .notebook(id) + .topics.topic("General") + .add(noteId); + await db.notebooks.delete(id); + await db.trash.restore(db.trash.all[0].id); + let notebook = db.notebooks.notebook(id); + expect(notebook).toBeDefined(); + expect(db.notes.note(noteId).notebook.id).toBe(id); + expect(db.notes.note(noteId).notebook.topic).toBe("General"); + })); + +test("restore a notebook that has deleted notes", () => + notebookTest().then(async ({ db, id }) => { + let noteId = await db.notes.add(TEST_NOTE); + await db.notebooks + .notebook(id) + .topics.topic("General") + .add(noteId); + await db.notebooks.delete(id); + await db.notes.delete(noteId); + await db.trash.restore(db.trash.all[0].id); + let notebook = db.notebooks.notebook(id); + expect(notebook).toBeDefined(); + expect(notebook.topics.topic("General").has(noteId)).toBe(false); + })); diff --git a/packages/core/api/index.js b/packages/core/api/index.js index 914d0e39b..a2a63f4d0 100644 --- a/packages/core/api/index.js +++ b/packages/core/api/index.js @@ -1,5 +1,6 @@ import Notes from "../collections/notes"; import Notebooks from "../collections/notebooks"; +import Trash from "../collections/trash"; class DB { constructor(context) { @@ -8,8 +9,10 @@ class DB { async init() { this.notebooks = new Notebooks(this.context); this.notes = new Notes(this.context); - await this.notes.init(this.notebooks); - await this.notebooks.init(this.notes); + this.trash = new Trash(this.context); + await this.notes.init(this.notebooks, this.trash); + await this.notebooks.init(this.notes, this.trash); + await this.trash.init(this.notes, this.notebooks); } } diff --git a/packages/core/collections/notebooks.js b/packages/core/collections/notebooks.js index 6cbf7cd2d..bc4d88868 100644 --- a/packages/core/collections/notebooks.js +++ b/packages/core/collections/notebooks.js @@ -1,6 +1,8 @@ import CachedCollection from "../database/cached-collection"; import fuzzysearch from "fuzzysearch"; import Notebook from "../models/notebook"; +import Notes from "./notes"; +import Trash from "./trash"; var tfun = require("transfun/transfun.js").tfun; if (!tfun) { tfun = global.tfun; @@ -12,8 +14,14 @@ export default class Notebooks { this.notes = undefined; } - init(notes) { + /** + * + * @param {Notes} notes + * @param {Trash} trash + */ + init(notes, trash) { this.notes = notes; + this.trash = trash; return this.collection.init(); } @@ -37,20 +45,19 @@ export default class Notebooks { notebook.topics = [...notebook.topics, ...oldNotebook.topics]; } - let topics = notebook.topics || []; + let topics = + notebook.topics.filter((v, i) => notebook.topics.indexOf(v) === i) || []; if (!oldNotebook) { topics[0] = makeTopic("General", id); } for (let i = 0; i < topics.length; i++) { let topic = topics[i]; - let isDuplicate = - topics.findIndex(t => t.title === (topic || topic.title)) > -1; //check for duplicate let isEmpty = !topic || (typeof topic === "string" && topic.trim().length <= 0); - if (isEmpty || isDuplicate) { + if (isEmpty) { topics.splice(i, 1); i--; continue; @@ -96,6 +103,7 @@ export default class Notebooks { notebook.topics.delete(...notebook.topics.all) ); await this.collection.removeItem(id); + await this.trash.add(notebook.data); } } diff --git a/packages/core/collections/notes.js b/packages/core/collections/notes.js index 3325d2f0e..3ec5ee64f 100644 --- a/packages/core/collections/notes.js +++ b/packages/core/collections/notes.js @@ -11,6 +11,7 @@ import { import Storage from "../database/storage"; import Notebooks from "./notebooks"; import Note from "../models/note"; +import Trash from "./trash"; var tfun = require("transfun/transfun.js").tfun; if (!tfun) { tfun = global.tfun; @@ -26,10 +27,12 @@ export default class Notes { /** * * @param {Notebooks} notebooks + * @param {Trash} trash */ - async init(notebooks) { + async init(notebooks, trash) { await this.collection.init(); this.notebooks = notebooks; + this.trash = trash; await this.tagsCollection.init(); } @@ -175,7 +178,7 @@ export default class Notes { await this.tagsCollection.remove(tag); } await this.collection.removeItem(id); - this.deltaStorage.remove(id + "_delta"); + await this.trash.add(item.data); } } diff --git a/packages/core/collections/topics.js b/packages/core/collections/topics.js index ea015bd34..a85273288 100644 --- a/packages/core/collections/topics.js +++ b/packages/core/collections/topics.js @@ -15,6 +15,10 @@ export default class Topics { this.notes = notes; } + exists(topic) { + return this.all.findIndex(v => v.title === (topic.title || topic)) > -1; + } + async add(topic) { await this.notebooks.add({ id: this.notebookId, @@ -36,23 +40,20 @@ export default class Topics { } async delete(...topics) { - let notebook = this.notebooks.notebook(this.notebookId); - if (!notebook) return; - notebook = notebook.data; - for (let topic of topics) { + let allTopics = JSON.parse(JSON.stringify(this.all)); //FIXME: make a deep copy + for (let i = 0; i < allTopics.length; i++) { + let topic = allTopics[i]; if (!topic) continue; - let index = notebook.topics.findIndex( - t => t.title === topic.title || topic - ); - if (index <= -1) continue; - topic = notebook.topics[index]; + let index = topics.findIndex(t => (t.title || t) === topic.title); let t = this.topic(topic); await t.transaction(() => t.delete(...topic.notes), false); - notebook.topics.splice(index, 1); + if (index > -1) { + allTopics.splice(i, 1); + } } await this.notebooks.add({ - id: notebook.id, - topics: notebook.topics + id: this.notebookId, + topics: allTopics }); } } diff --git a/packages/core/collections/trash.js b/packages/core/collections/trash.js new file mode 100644 index 000000000..93cbb2b4e --- /dev/null +++ b/packages/core/collections/trash.js @@ -0,0 +1,93 @@ +import CachedCollection from "../database/cached-collection"; +import Notes from "./notes"; +import Notebooks from "./notebooks"; +import Storage from "../database/storage"; +export default class Trash { + constructor(context) { + this.collection = new CachedCollection(context, "trash"); + this.deltaStorage = new Storage(context); + } + + /** + * + * @param {Notes} notes + * @param {Notebooks} notebooks + */ + async init(notes, notebooks) { + this.notes = notes; + this.notebooks = notebooks; + await this.collection.init(); + } + + get all() { + return this.collection.getAllItems(); + } + + async add(item) { + if (this.collection.exists(item.id + "_deleted")) + throw new Error("This item has already been deleted."); + item.dateDeleted = Date.now(); + item.id = item.id + "_deleted"; + await this.collection.addItem(item); + } + + async delete(...ids) { + for (let id of ids) { + if (!this.collection.exists(id)) return; + if (id.indexOf("note") > -1) + this.deltaStorage.remove(id.replace("_deleted", "") + "_delta"); + await this.collection.removeItem(id); + } + } + + async restore(...ids) { + for (let id of ids) { + let item = this.collection.getItem(id); + if (!item) continue; + delete item.dateDeleted; + item.id = item.id.replace("_deleted", ""); + if (item.type === "note") { + let { notebook } = item; + item.notebook = {}; + await this.notes.add(item); + + if (notebook && notebook.id && notebook.topic) { + const { id, topic } = notebook; + + // if the notebook has been deleted + if (!this.notebooks.collection.exists(id)) { + notebook = {}; + } else { + // if the topic has been deleted + if (!this.notebooks.notebook(id).topics.exists(topic)) { + notebook = {}; + } + } + + // restore the note to the topic it was in before deletion + if (notebook.id && notebook.topic) { + await this.notebooks + .notebook(id) + .topics.topic(topic) + .add(item.id); + } + } + } else { + const { topics } = item; + item.topics = []; + await this.notebooks.add(item); + let notebook = this.notebooks.notebook(item.id); + for (let topic of topics) { + let t = await notebook.topics.add(topic.title); + await t.add(...topic.notes); + } + } + await this.collection.removeItem(id); + } + } + + async clear() { + let indices = await this.collection.indexer.getIndices(); + return this.delete(...indices); + } +} diff --git a/packages/core/database/cached-collection.js b/packages/core/database/cached-collection.js index f9f9a0a25..1a67b0562 100644 --- a/packages/core/database/cached-collection.js +++ b/packages/core/database/cached-collection.js @@ -50,6 +50,10 @@ export default class CachedCollection { } } + exists(id) { + return this.map.has(id); + } + getItem(id) { return this.map.get(id); }