diff --git a/packages/core/__tests__/migrations.test.ts b/packages/core/__tests__/migrations.test.ts
new file mode 100644
index 000000000..2c0ea3666
--- /dev/null
+++ b/packages/core/__tests__/migrations.test.ts
@@ -0,0 +1,663 @@
+/*
+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
hello
hello
hello
hello
hello
<div class="table-container" contenteditable="false"><table contenteditable="true"></table></div>` + }; + expect(await migrateItem(item, 5.3, 5.4, "tiny", db, "local")).toBe(true); + expect(item.data).toBe( + `hello
Function
\nHello
\nfunction google() {
var function google();
Function google();
}\nHh
\n`, + to: `
Function
Hello
function google() {
var function google();
Function google();
}Hh
` + }, + { + name: "replace table container with table", + from: `
hello
hello

`
+ },
+ {
+ name: "replace [data-mce-bogus] elements",
+ from: `Hello world2
`, + to: `Hello world2
` + } + ]; + + for (const testCase of cases) { + test(testCase.name, () => + databaseTest().then(async (db) => { + const item = { + type: "tiny", + dateEdited: Date.now(), + data: testCase.from + }; + expect(await migrateItem(item, 5.4, 5.5, "tiny", db, "local")).toBe( + true + ); + expect(item.data).toBe(testCase.to); + }) + ); + } +}); + +test("[5.6] remove note ids from topics", () => + databaseTest().then(async (db) => { + const item = { + type: "notebook", + topics: [{ notes: ["helloworld"] }, { notes: ["helloworld2"] }] + }; + expect(await migrateItem(item, 5.6, 5.7, "notebook", db, "local")).toBe( + true + ); + expect(item.topics.every((t) => !t.notes)).toBe(true); + })); + +test("[5.6] move pins to shortcuts", () => + databaseTest().then(async (db) => { + const item = { + type: "settings", + pins: [ + { + type: "topic", + data: { + id: "hello", + notebookId: "world" + } + }, + { + type: "notebook", + data: { id: "world" } + }, + { + type: "tag", + data: { id: "tag" } + } + ] + }; + expect(await migrateItem(item, 5.6, 5.7, "settings", db, "local")).toBe( + true + ); + expect(item.pins).toBeUndefined(); + expect( + db.shortcuts.all.find( + (s) => + s.item.type === "topic" && + s.item.id === "hello" && + s.item.notebookId === "world" + ) + ).toBeDefined(); + expect( + db.shortcuts.all.find( + (s) => s.item.type === "notebook" && s.item.id === "world" + ) + ).toBeDefined(); + expect( + db.shortcuts.all.find((s) => s.item.type === "tag" && s.item.id === "tag") + ).toBeDefined(); + })); + +test("[5.7] change session content type from tiny to tiptap", () => + databaseTest().then(async (db) => { + const item = { + id: "hello_content", + type: "tiny", + data: "hello world
" + }; + expect(await migrateItem(item, 5.7, 5.8, "tiny", db, "local")).toBe(true); + expect(item.type).toBe("sessioncontent"); + expect(item.contentType).toBe("tiptap"); + })); + +test("[5.7] change content item type to tiptap", () => + databaseTest().then(async (db) => { + const item = { + id: "hello_content", + type: "content", + data: "hello world
" + }; + expect(await migrateItem(item, 5.7, 5.8, "content", db, "local")).toBe( + true + ); + expect(item.type).toBe("tiptap"); + })); + +test("[5.7] change shortcut item id to be same as its reference id", () => + databaseTest().then(async (db) => { + const item = { + id: "something", + type: "shortcut", + item: { + type: "notebook", + id: "world" + } + }; + expect(await migrateItem(item, 5.7, 5.8, "shortcut", db, "local")).toBe( + true + ); + expect(item.id).toBe("world"); + })); + +test("[5.7] add type to session content", () => + databaseTest().then(async (db) => { + const item = { + id: "hello_content", + type: "tiptap", + data: "hello world
" + }; + expect(await migrateItem(item, 5.7, 5.8, "tiptap", db, "local")).toBe(true); + expect(item.type).toBe("sessioncontent"); + expect(item.contentType).toBe("tiptap"); + })); + +test("[5.7] change notehistory type to session", () => + databaseTest().then(async (db) => { + const item = { + type: "notehistory" + }; + expect(await migrateItem(item, 5.7, 5.8, "notehistory", db, "local")).toBe( + true + ); + expect(item.type).toBe("session"); + })); + +test("[5.8] remove remote property from items", () => + databaseTest().then(async (db) => { + const item = { + type: "note", + remote: true + }; + expect(await migrateItem(item, 5.8, 5.9, "note", db, "local")).toBe(true); + expect(item.remote).toBeUndefined(); + })); + +test("[5.8] do nothing if backup type is not local", () => + databaseTest().then(async (db) => { + const item = { + type: "note", + remote: true + }; + expect(await migrateItem(item, 5.8, 5.9, "note", db, "backup")).toBe(false); + expect(await migrateItem(item, 5.8, 5.9, "note", db, "sync")).toBe(false); + })); + +describe("[5.9] make tags syncable", () => { + test("create tags inside notes & link to them using relations", () => + databaseTest().then(async (db) => { + const noteId = getId(); + const tags = ["hello", "world", "i am here"]; + await db.notes.collection.add({ + type: "note", + title: "I am a note", + tags, + id: noteId + }); + + for (const tag of tags) { + await db.tags.collection.add({ + id: makeId(tag), + noteIds: [noteId], + type: "tag", + title: tag + }); + } + await db.storage().write("settings", { + ...db.legacySettings.raw, + aliases: { + [makeId(tags[1])]: "I AM GOOD!" + } + }); + await db.legacySettings.init(); + + const note = db.notes.note(noteId); + if (!note) throw new Error("Failed to find note."); + + expect(await migrateItem(note.data, 5.9, 6.0, "note", db, "backup")).toBe( + true + ); + + const resolvedTags = db.relations + .to({ type: "note", id: noteId }, "tag") + .resolved() + .sort((a, b) => a.title.localeCompare(b.title)); + + expect(note.data.tags).toBeUndefined(); + expect(db.tags.all).toHaveLength(3); + expect(resolvedTags).toHaveLength(3); + expect(resolvedTags[0].title).toBe("hello"); + expect(resolvedTags[1].title).toBe("I AM GOOD!"); + expect(resolvedTags[2].title).toBe("i am here"); + expect( + tags.every((t) => !db.tags.collection.exists(makeId(t))) + ).toBeTruthy(); + })); + + test("migrate old tag item to new one", () => + databaseTest().then(async (db) => { + const tag = { + id: makeId("oldone"), + noteIds: [], + type: "tag", + title: "oldone" + }; + expect(await migrateItem(tag, 5.9, 6.0, "tag", db, "backup")).toBe(true); + expect(tag.id).not.toBe(makeId("oldone")); + expect(tag.noteIds).toBeUndefined(); + expect(tag.alias).toBeUndefined(); + expect(tag.title).toBe("oldone"); + })); + + test("migrate old tag item with alias to new one", () => + databaseTest().then(async (db) => { + await db.storage().write("settings", { + ...db.legacySettings.raw, + aliases: { + [makeId("oldone")]: "alias" + } + }); + await db.legacySettings.init(); + + const tag = { + id: makeId("oldone"), + noteIds: [], + type: "tag", + title: "oldone" + }; + expect(await migrateItem(tag, 5.9, 6.0, "tag", db, "backup")).toBe(true); + expect(tag.id).not.toBe(makeId("oldone")); + expect(tag.noteIds).toBeUndefined(); + expect(tag.alias).toBeUndefined(); + expect(tag.title).toBe("alias"); + })); + + test("migrate tags before notes", () => + databaseTest().then(async (db) => { + const noteId = getId(); + const tags = ["hello", "world", "i am here"]; + await db.notes.collection.add({ + type: "note", + title: "I am a note", + tags, + id: noteId + }); + await db.storage().write("settings", { + ...db.legacySettings.raw, + aliases: { + [makeId(tags[1])]: "I AM GOOD!" + } + }); + await db.legacySettings.init(); + for (const tag of tags) { + const item = { + id: makeId(tag), + noteIds: [noteId], + type: "tag", + title: tag + }; + await migrateItem(item, 5.9, 6.0, "tag", db, "backup"); + await db.tags.collection.add(item); + } + + const note = db.notes.note(noteId); + if (!note) throw new Error("Failed to find note."); + + expect(await migrateItem(note.data, 5.9, 6.0, "note", db, "backup")).toBe( + true + ); + + const resolvedTags = db.relations + .to({ type: "note", id: noteId }, "tag") + .resolved() + .sort((a, b) => a.title.localeCompare(b.title)); + + expect(note.data.tags).toBeUndefined(); + expect(db.tags.all).toHaveLength(3); + expect(resolvedTags).toHaveLength(3); + expect(resolvedTags[0].title).toBe("hello"); + expect(resolvedTags[1].title).toBe("I AM GOOD!"); + expect(resolvedTags[2].title).toBe("i am here"); + expect( + tags.every((t) => !db.tags.collection.exists(makeId(t))) + ).toBeTruthy(); + })); +}); + +describe("[5.9] make colors syncable", () => { + test("create colors from notes & link to them using relations", () => + databaseTest().then(async (db) => { + const noteId = getId(); + await db.notes.collection.add({ + type: "note", + title: "I am a note", + color: "blue", + id: noteId + }); + + await db.colors.collection.add({ + id: makeId("blue"), + noteIds: [noteId], + type: "tag", + title: "blue" + }); + + const note = db.notes.note(noteId); + if (!note) throw new Error("Failed to find note."); + + expect(await migrateItem(note.data, 5.9, 6.0, "note", db, "backup")).toBe( + true + ); + + const resolvedColors = db.relations + .to({ type: "note", id: noteId }, "color") + .resolved(); + + expect(note.data.color).toBeUndefined(); + expect(db.colors.all).toHaveLength(1); + expect(resolvedColors).toHaveLength(1); + expect(resolvedColors[0].title).toBe("blue"); + expect(resolvedColors[0].colorCode).toBe("#2196F3"); + expect(db.colors.collection.exists(makeId("blue"))).toBeFalsy(); + })); + + test("migrate old color item to new one", () => + databaseTest().then(async (db) => { + const color = { + id: makeId("blue"), + noteIds: [], + type: "tag", + title: "blue" + }; + expect(await migrateItem(color, 5.9, 6.0, "tag", db, "backup")).toBe( + true + ); + expect(color.id).not.toBe(makeId("oldone")); + expect(color.noteIds).toBeUndefined(); + expect(color.alias).toBeUndefined(); + expect(color.title).toBe("blue"); + expect(color.type).toBe("color"); + expect(color.colorCode).toBe("#2196F3"); + })); + + test("migrate old color item with alias to new one", () => + databaseTest().then(async (db) => { + await db.storage().write("settings", { + ...db.legacySettings.raw, + aliases: { + [makeId("blue")]: "very important" + } + }); + await db.legacySettings.init(); + + const color = { + id: makeId("blue"), + noteIds: [], + type: "tag", + title: "blue" + }; + expect(await migrateItem(color, 5.9, 6.0, "tag", db, "backup")).toBe( + true + ); + expect(color.id).not.toBe(makeId("oldone")); + expect(color.noteIds).toBeUndefined(); + expect(color.alias).toBeUndefined(); + expect(color.title).toBe("very important"); + expect(color.type).toBe("color"); + expect(color.colorCode).toBe("#2196F3"); + })); + + test("migrate color before notes", () => + databaseTest().then(async (db) => { + const noteId = getId(); + await db.notes.collection.add({ + type: "note", + title: "I am a note", + color: "blue", + id: noteId + }); + await db.storage().write("settings", { + ...db.legacySettings.raw, + aliases: { + [makeId("blue")]: "I AM GOOD!" + } + }); + await db.legacySettings.init(); + + const color = { + id: makeId("blue"), + noteIds: [noteId], + type: "tag", + title: "blue" + }; + await migrateItem(color, 5.9, 6.0, "tag", db, "backup"); + await db.colors.collection.add(color); + + const note = db.notes.note(noteId); + if (!note) throw new Error("Failed to find note."); + + expect(await migrateItem(note.data, 5.9, 6.0, "note", db, "backup")).toBe( + true + ); + + const resolvedColors = db.relations + .to({ type: "note", id: noteId }, "color") + .resolved(); + + expect(note.data.color).toBeUndefined(); + expect(db.colors.all).toHaveLength(1); + expect(resolvedColors).toHaveLength(1); + expect(resolvedColors[0].title).toBe("I AM GOOD!"); + expect(resolvedColors[0].colorCode).toBe("#2196F3"); + expect(db.colors.collection.exists(makeId("blue"))).toBeFalsy(); + })); +}); + +test("[5.9] move attachments.noteIds to relations", () => + databaseTest().then(async (db) => { + const attachment = { + id: "ATTACHMENT_ID", + type: "attachment", + noteIds: ["HELLO_NOTE_ID"] + }; + await migrateItem(attachment, 5.9, 6.0, "attachment", db, "backup"); + + const linkedNotes = db.relations.from( + { type: "attachment", id: "ATTACHMENT_ID" }, + "note" + ); + expect(attachment.noteIds).toBeUndefined(); + expect(linkedNotes).toHaveLength(1); + expect(linkedNotes[0].to.id).toBe("HELLO_NOTE_ID"); + })); + +describe("[5.9] move topics out of notebooks & use relations", () => { + test("convert topics to subnotebooks", () => + databaseTest().then(async (db) => { + const notebook = { + id: "parent_notebook", + type: "notebook", + topics: [ + { id: "topics1", title: "Topic 1" }, + { id: "topics2", title: "Topic 2" } + ] + }; + await migrateItem(notebook, 5.9, 6.0, "notebook", db, "backup"); + + const linkedNotebooks = db.relations + .from({ type: "notebook", id: "parent_notebook" }, "notebook") + .sort((a, b) => a.to.id.localeCompare(b.to.id)); + expect(notebook.topics).toBeUndefined(); + expect(linkedNotebooks).toHaveLength(2); + expect(linkedNotebooks[0].to.id).toBe("topics1"); + expect(linkedNotebooks[1].to.id).toBe("topics2"); + expect(db.notebooks.all).toHaveLength(2); + expect(db.notebooks.notebook("topics1")).toBeDefined(); + expect(db.notebooks.notebook("topics2")).toBeDefined(); + })); + + test("convert topic shortcuts to notebook shortcuts", () => + databaseTest().then(async (db) => { + const shortcut = { + id: "shortcut1", + type: "shortcut", + item: { + type: "topic", + id: "topics1" + } + }; + await migrateItem(shortcut, 5.9, 6.0, "shortcut", db, "backup"); + + expect(shortcut.item.type).toBe("notebook"); + expect(shortcut.item.id).toBe("topics1"); + })); + + test("convert topic links in note to relations", () => + databaseTest().then(async (db) => { + const note = { + id: "note1", + type: "note", + notebooks: [{ id: "notebook1", topics: ["topic1", "topic2"] }] + }; + await migrateItem(note, 5.9, 6.0, "note", db, "backup"); + + const linkedNotebooks = db.relations + .to({ type: "note", id: "note1" }, "notebook") + .sort(); + expect(note.notebooks).toBeUndefined(); + expect(linkedNotebooks).toHaveLength(2); + expect(linkedNotebooks[0].from.id).toBe("topic1"); + expect(linkedNotebooks[1].from.id).toBe("topic2"); + })); +}); + +test("[5.9] migrate settings to its own collection", () => + databaseTest().then(async (db) => { + const settings: LegacySettingsItem = { + type: "settings", + id: "settings", + dateCreated: Date.now(), + dateModified: Date.now(), + dateFormat: "CUSTOM_DATE_FORMAT!", + defaultNotebook: { id: "notebook1", topic: "topic1" }, + groupOptions: { + favorites: { + groupBy: "abc", + sortBy: "dateCreated", + sortDirection: "asc" + } + }, + timeFormat: "24-hour", + titleFormat: "I AM TITLE FORMAT", + toolbarConfig: { desktop: { preset: "custom" } }, + trashCleanupInterval: 365 + }; + await migrateItem(settings, 5.9, 6.0, "settings", db, "backup"); + + expect(db.settings.getDateFormat()).toBe(settings.dateFormat); + expect(db.settings.getDefaultNotebook()).toBe( + settings.defaultNotebook?.topic + ); + expect(db.settings.getGroupOptions("favorites")).toMatchObject( + settings.groupOptions?.favorites || {} + ); + expect(db.settings.getTitleFormat()).toBe(settings.titleFormat); + expect(db.settings.getTimeFormat()).toBe(settings.timeFormat); + expect(db.settings.getToolbarConfig("desktop")).toMatchObject( + settings.toolbarConfig?.desktop || {} + ); + expect(db.settings.getTrashCleanupInterval()).toBe( + settings.trashCleanupInterval + ); + })); diff --git a/packages/core/src/api/migrations.ts b/packages/core/src/api/migrations.ts index bebbf104f..da88de8d5 100644 --- a/packages/core/src/api/migrations.ts +++ b/packages/core/src/api/migrations.ts @@ -45,6 +45,14 @@ class Migrations { await this.db.notes.init(); const collections: MigratableCollections = [ + { + items: () => [this.db.legacySettings.raw], + type: "settings" + }, + { + items: () => this.db.settings.raw, + type: "settingsv2" + }, { items: () => this.db.attachments.all, type: "attachments" @@ -65,10 +73,6 @@ class Migrations { iterate: true, type: "content" }, - { - items: () => [this.db.settings.raw], - type: "settings" - }, { items: () => this.db.shortcuts.raw, type: "shortcuts" diff --git a/packages/core/src/api/sync/index.ts b/packages/core/src/api/sync/index.ts index 713ede51e..0a1c4d159 100644 --- a/packages/core/src/api/sync/index.ts +++ b/packages/core/src/api/sync/index.ts @@ -19,6 +19,7 @@ along with this program. If not, see