feat: migrate from tiny -> tiptap

This commit is contained in:
thecodrr
2022-07-07 13:17:55 +05:00
parent 81206cb949
commit 3f54f36d50
16 changed files with 125 additions and 45 deletions

View File

@@ -104,7 +104,7 @@ describe.each([
const allContent = await db.content.all(); const allContent = await db.content.all();
expect( expect(
allContent.every((v) => v.type === "tiny" || v.deleted) allContent.every((v) => v.type === "tiptap" || v.deleted)
).toBeTruthy(); ).toBeTruthy();
expect(allContent.every((v) => !v.persistDateEdited)).toBeTruthy(); expect(allContent.every((v) => !v.persistDateEdited)).toBeTruthy();
expect(allContent.every((v) => v.dateModified > 0)).toBeTruthy(); expect(allContent.every((v) => v.dateModified > 0)).toBeTruthy();

View File

@@ -42,7 +42,7 @@ test("search notes with an empty note", () =>
}).then(async ({ db }) => { }).then(async ({ db }) => {
await db.notes.add({ await db.notes.add({
title: "world is a heavy tune", title: "world is a heavy tune",
content: { type: "tiny", data: "<p><br></p>" }, content: { type: "tiptap", data: "<p><br></p>" },
}); });
let filtered = await db.lookup.notes(db.notes.all, "heavy tune"); let filtered = await db.lookup.notes(db.notes.all, "heavy tune");
expect(filtered).toHaveLength(1); expect(filtered).toHaveLength(1);

View File

@@ -8,7 +8,7 @@ beforeEach(async () => {
// let note = await db.notes.note(noteId).data; // let note = await db.notes.note(noteId).data;
// let content = { // let content = {
// data: await db.notes.note(noteId).content(), // data: await db.notes.note(noteId).content(),
// type: "tiny", // type: "tiptap",
// }; // };
// let session = await db.noteHistory.add(noteId, note.dateEdited, content); // let session = await db.noteHistory.add(noteId, note.dateEdited, content);
@@ -28,7 +28,7 @@ test("editing the same note should create multiple history sessions", () =>
noteTest({ ...TEST_NOTE, sessionId: Date.now() }).then(async ({ db, id }) => { noteTest({ ...TEST_NOTE, sessionId: Date.now() }).then(async ({ db, id }) => {
let editedContent = { let editedContent = {
data: TEST_NOTE.content.data + "<p>Some new content</p>", data: TEST_NOTE.content.data + "<p>Some new content</p>",
type: "tiny", type: "tiptap",
}; };
await db.notes.add({ await db.notes.add({
@@ -52,7 +52,7 @@ test("restoring an old session should replace note's content", () =>
noteTest({ ...TEST_NOTE, sessionId: Date.now() }).then(async ({ db, id }) => { noteTest({ ...TEST_NOTE, sessionId: Date.now() }).then(async ({ db, id }) => {
let editedContent = { let editedContent = {
data: TEST_NOTE.content.data + "<p>Some new content</p>", data: TEST_NOTE.content.data + "<p>Some new content</p>",
type: "tiny", type: "tiptap",
}; };
await db.notes.add({ await db.notes.add({
@@ -75,7 +75,7 @@ test("date created of session should not change on edit", () =>
let editedContent = { let editedContent = {
data: TEST_NOTE.content.data + "<p>Some new content</p>", data: TEST_NOTE.content.data + "<p>Some new content</p>",
type: "tiny", type: "tiptap",
}; };
await delay(1000); await delay(1000);
@@ -141,7 +141,7 @@ test("auto clear sessions if they exceed the limit", () =>
noteTest({ ...TEST_NOTE, sessionId: Date.now() }).then(async ({ db, id }) => { noteTest({ ...TEST_NOTE, sessionId: Date.now() }).then(async ({ db, id }) => {
let editedContent = { let editedContent = {
data: TEST_NOTE.content.data + "<p>Some new content</p>", data: TEST_NOTE.content.data + "<p>Some new content</p>",
type: "tiny", type: "tiptap",
}; };
await db.notes.add({ await db.notes.add({
@@ -168,7 +168,7 @@ test("save a locked note should add a locked session to note history", () =>
await db.vault.add(id); await db.vault.add(id);
const note = db.notes.note(id).data; const note = db.notes.note(id).data;
const editedContent = { type: "tiny", data: "<p>hello world</p>" }; const editedContent = { type: "tiptap", data: "<p>hello world</p>" };
await db.vault.save({ await db.vault.save({
...note, ...note,
content: editedContent, content: editedContent,

View File

@@ -318,7 +318,7 @@ test("note content should not contain image base64 data after save", () =>
id id
); );
await db.notes.add({ id, content: { type: "tiny", data: IMG_CONTENT } }); await db.notes.add({ id, content: { type: "tiptap", data: IMG_CONTENT } });
const note = db.notes.note(id); const note = db.notes.note(id);
const content = await note.content(); const content = await note.content();
expect(content).not.toContain(`src="data:image/png;`); expect(content).not.toContain(`src="data:image/png;`);

View File

@@ -29,7 +29,7 @@ const notebookTest = (notebook = TEST_NOTEBOOK) =>
var TEST_NOTE = { var TEST_NOTE = {
content: { content: {
type: "tiny", type: "tiptap",
data: `<p>Hello<br><span style="color:#f00">This is colorful</span></p>`, data: `<p>Hello<br><span style="color:#f00">This is colorful</span></p>`,
}, },
}; };

View File

@@ -108,7 +108,7 @@ test("save an edited locked note", () =>
const note = db.notes.note(id).data; const note = db.notes.note(id).data;
await db.vault.save({ await db.vault.save({
...note, ...note,
content: { type: "tiny", data: "<p>hello world</p>" }, content: { type: "tiptap", data: "<p>hello world</p>" },
}); });
const content = await db.content.raw(note.contentId); const content = await db.content.raw(note.contentId);

View File

@@ -1,4 +1,4 @@
import { CURRENT_DATABASE_VERSION, EV, EVENTS } from "../common"; import { CURRENT_DATABASE_VERSION, EVENTS } from "../common";
import Migrator from "../database/migrator"; import Migrator from "../database/migrator";
class Migrations { class Migrations {
@@ -9,6 +9,7 @@ class Migrations {
constructor(db) { constructor(db) {
this._db = db; this._db = db;
this._migrator = new Migrator(); this._migrator = new Migrator();
this._isMigrating = false;
} }
async init() { async init() {
@@ -18,7 +19,13 @@ class Migrations {
} }
async migrate() { async migrate() {
if (this.dbVersion >= CURRENT_DATABASE_VERSION) return; if (this.dbVersion >= CURRENT_DATABASE_VERSION || this._isMigrating) return;
this._isMigrating = true;
this._db.eventManager.publish(EVENTS.databaseMigrating, {
from: this.dbVersion,
to: CURRENT_DATABASE_VERSION,
});
await this._db.notes.init(); await this._db.notes.init();
const content = await this._db.content.all(); const content = await this._db.content.all();
@@ -58,10 +65,7 @@ class Migrations {
await this._migrator.migrate(collections, (item) => item, this.dbVersion); await this._migrator.migrate(collections, (item) => item, this.dbVersion);
await this._db.storage.write("v", CURRENT_DATABASE_VERSION); await this._db.storage.write("v", CURRENT_DATABASE_VERSION);
EV.publish(EVENTS.databaseMigrated, { this._db.eventManager.publish(EVENTS.databaseMigrated);
prev: this.dbVersion,
current: CURRENT_DATABASE_VERSION,
});
this.dbVersion = CURRENT_DATABASE_VERSION; this.dbVersion = CURRENT_DATABASE_VERSION;
} }

View File

@@ -122,7 +122,7 @@ test.skip(
for (let i = 0; i < 10; ++i) { for (let i = 0; i < 10; ++i) {
const id = await deviceA.notes.add({ const id = await deviceA.notes.add({
content: { content: {
type: "tiny", type: "tiptap",
data: `<p>deviceA=true</p>`, data: `<p>deviceA=true</p>`,
}, },
}); });
@@ -137,7 +137,7 @@ test.skip(
const noteId = await deviceA.notes.add({ const noteId = await deviceA.notes.add({
id, id,
content: { content: {
type: "tiny", type: "tiptap",
data: `<p>deviceA=true+changed=true</p>`, data: `<p>deviceA=true+changed=true</p>`,
}, },
}); });
@@ -162,7 +162,7 @@ test.skip(
await deviceB.notes.add({ await deviceB.notes.add({
id, id,
content: { content: {
type: "tiny", type: "tiptap",
data: "<p>changes from device B</p>", data: "<p>changes from device B</p>",
}, },
}); });
@@ -211,7 +211,7 @@ test.skip(
for (let i = 0; i < 3; ++i) { for (let i = 0; i < 3; ++i) {
const id = await deviceA.notes.add({ const id = await deviceA.notes.add({
content: { content: {
type: "tiny", type: "tiptap",
data: `<p>deviceA=true</p>`, data: `<p>deviceA=true</p>`,
}, },
}); });

View File

@@ -99,7 +99,7 @@ class Merger {
if (deserialized.alg && deserialized.cipher) return deserialized; if (deserialized.alg && deserialized.cipher) return deserialized;
let type = deserialized.type; let type = deserialized.type;
if (!type && deserialized.data) type = "tiny"; if (!type && deserialized.data) type = "tiptap";
if (!migrations[version]) { if (!migrations[version]) {
throw new Error( throw new Error(
@@ -110,7 +110,10 @@ class Merger {
} }
const migrate = migrations[version][type]; const migrate = migrations[version][type];
if (migrate) return migrate(deserialized); if (migrate) {
console.log(migrate, version, type);
return migrate(deserialized);
}
return deserialized; return deserialized;
} }

View File

@@ -1,4 +1,5 @@
import { CHECK_IDS, EV, EVENTS, checkIsUserPremium } from "../common"; import { CHECK_IDS, EV, EVENTS, checkIsUserPremium } from "../common";
import { tinyToTiptap } from "../migrations";
const ERASE_TIME = 1000 * 60 * 30; const ERASE_TIME = 1000 * 60 * 30;
var ERASER_TIMEOUT = null; var ERASER_TIMEOUT = null;
@@ -228,7 +229,18 @@ export default class Vault {
encryptedContent.data encryptedContent.data
); );
return { type: encryptedContent.type, data: JSON.parse(decryptedContent) }; const content = {
type: encryptedContent.type,
data: JSON.parse(decryptedContent),
};
// #MIGRATION: convert tiny to tiptap
if (content.type === "tiny") {
content.type = "tiptap";
content.data = tinyToTiptap(content.data);
}
return content;
} }
/** @private */ /** @private */

View File

@@ -52,6 +52,7 @@ export const EVENTS = {
syncProgress: "sync:progress", syncProgress: "sync:progress",
syncCompleted: "sync:completed", syncCompleted: "sync:completed",
databaseMigrated: "db:migrated", databaseMigrated: "db:migrated",
databaseMigrating: "db:migrating",
databaseUpdated: "db:updated", databaseUpdated: "db:updated",
databaseCollectionInitiated: "db:collectionInitiated", databaseCollectionInitiated: "db:collectionInitiated",
appRefreshRequested: "app:refreshRequested", appRefreshRequested: "app:refreshRequested",
@@ -65,7 +66,7 @@ export const EVENTS = {
systemTimeInvalid: "system:invalidTime", systemTimeInvalid: "system:invalidTime",
}; };
export const CURRENT_DATABASE_VERSION = 5.4; export const CURRENT_DATABASE_VERSION = 5.5;
export function setUserPersonalizationBytes(userSalt) { export function setUserPersonalizationBytes(userSalt) {
USER_PERSONALIZATION_HASH = new Uint8Array( USER_PERSONALIZATION_HASH = new Uint8Array(

View File

@@ -1,9 +1,9 @@
import Tiny from "./tiny"; import { Tiptap } from "./tiptap";
export function getContentFromData(type, data) { export function getContentFromData(type, data) {
switch (type) { switch (type) {
case "tiny": case "tiptap":
return new Tiny(data); return new Tiptap(data);
default: default:
return null; return null;
} }

View File

@@ -6,7 +6,7 @@ var converter = new showdown.Converter();
converter.setFlavor("original"); converter.setFlavor("original");
const splitter = /\W+/gm; const splitter = /\W+/gm;
class Tiny { export class Tiptap {
constructor(data) { constructor(data) {
this.data = data; this.data = data;
this.text; this.text;
@@ -144,7 +144,6 @@ class Tiny {
}; };
} }
} }
export default Tiny;
function getDatasetAttribute(element, attribute) { function getDatasetAttribute(element, attribute) {
return element.getAttribute(`data-${attribute}`); return element.getAttribute(`data-${attribute}`);

View File

@@ -109,6 +109,7 @@ export default class Backup {
switch (version) { switch (version) {
case CURRENT_DATABASE_VERSION: case CURRENT_DATABASE_VERSION:
case 5.4:
case 5.3: case 5.3:
case 5.2: case 5.2:
case 5.1: case 5.1:

View File

@@ -13,27 +13,36 @@ export const migrations = {
tiny: (item) => { tiny: (item) => {
item = replaceDateEditedWithDateModified()(item); item = replaceDateEditedWithDateModified()(item);
if (!item.data || item.data.iv) return item; if (!item.data || item.data.iv) return migrations["5.3"].tiny(item);
item.data = removeToxClassFromChecklist(wrapTablesWithDiv(item.data)); item.data = removeToxClassFromChecklist(wrapTablesWithDiv(item.data));
return item; return migrations["5.3"].tiny(item);
}, },
settings: replaceDateEditedWithDateModified(true), settings: replaceDateEditedWithDateModified(true),
}, },
5.3: { 5.3: {
tiny: (item) => { tiny: (item) => {
if (!item.data || item.data.iv) return item; if (!item.data || item.data.iv) return migrations["5.4"].tiny(item);
item.data = decodeWrappedTableHtml(item.data); item.data = decodeWrappedTableHtml(item.data);
return item; return migrations["5.4"].tiny(item);
}, },
}, },
5.4: { 5.4: {
tiny: (item) => {
if (!item.data || item.data.iv) return item;
item.type = "tiptap";
item.data = tinyToTiptap(item.data);
return item;
},
},
5.5: {
note: false, note: false,
notebook: false, notebook: false,
tag: false, tag: false,
attachment: false, attachment: false,
trash: false, trash: false,
tiny: false, tiny: false,
tiptap: false,
settings: false, settings: false,
}, },
}; };
@@ -83,3 +92,54 @@ function decodeWrappedTableHtml(html) {
return html; return html;
}); });
} }
export function tinyToTiptap(html) {
const document = parseHTML(html);
const tables = document.querySelectorAll("table");
for (const table of tables) {
table.removeAttribute("contenteditable");
if (table.parentElement?.nodeName.toLowerCase() === "div") {
table.parentElement.replaceWith(table);
}
}
const images = document.querySelectorAll("p > img");
for (const image of images) {
image.parentElement.replaceWith(image.cloneNode());
}
const breaks = document.querySelectorAll("br");
for (const br of breaks) {
br.remove();
}
const paragraphs = document.querySelectorAll("p");
for (const p of paragraphs) {
if (!p.childNodes.length) p.remove();
}
const listItems = document.querySelectorAll("li");
for (const li of listItems) {
if (li.firstChild) {
const p = document.createElement("p");
p.appendChild(li.firstChild.cloneNode());
li.firstChild.replaceWith(p);
}
}
const bogus = document.querySelectorAll("[data-mce-bogus]");
for (const element of bogus) {
element.remove();
}
const attributes = document.querySelectorAll(
"[data-mce-href], [data-mce-flag]"
);
for (const element of attributes) {
element.removeAttribute("data-mce-href");
element.removeAttribute("data-mce-flag");
}
return document.body.innerHTML;
}