diff --git a/packages/core/__tests__/note-history.test.js b/packages/core/__tests__/note-history.test.js new file mode 100644 index 000000000..2d8fbd4ab --- /dev/null +++ b/packages/core/__tests__/note-history.test.js @@ -0,0 +1,181 @@ +import { compress, decompress } from "../utils/compression"; +import { databaseTest, noteTest, StorageInterface, TEST_NOTE } from "./utils"; + +beforeEach(async () => { + StorageInterface.clear(); +}); + + +async function sessionTest(db, noteId) { + let note = await db.notes.note(noteId).data; + let content = { + data: await db.notes.note(noteId).content(), + type: "tiny", + }; + let session = await db.noteHistory.add( + noteId, + note.dateEdited, + content + ); + + return session; +} + +test("History of note should be created", async () => { + let { db, id } = await noteTest(); + let session = await sessionTest(db, id); + let content = { + data: await db.notes.note(id).content(), + type: "tiny", + }; + + let sessionContent = await db.noteHistory.content(session.sessionContentId); + expect(sessionContent).toMatchObject(content); +}); + +test("Multiple sessions of the same note should be created", async () => { + let { db, id } = await noteTest(); + await sessionTest(db, id); + + let nextContent = { + data: (await db.notes.note(id).content().data) + "teststring", + type: "tiny", + }; + + await db.notes.add({ + id: id, + content: nextContent, + }); + let note = db.notes.note(id).data; + await db.noteHistory.add(id, note.dateEdited, nextContent); + + let history = await db.noteHistory.get(id); + expect(history.length).toBe(2); +}); + + +test("Session should be removed if greater than the version limit", async () => { + let { db, id } = await noteTest(); + await sessionTest(db, id); + + let nextContent = { + data: (await db.notes.note(id).content().data) + "teststring", + type: "tiny", + }; + + await db.notes.add({ + id: id, + content: nextContent, + }); + let note = db.notes.note(id).data; + await db.noteHistory.add(id, note.dateEdited, nextContent); + + let history = await db.noteHistory.get(id); + expect(history.length).toBe(2); + await db.noteHistory._cleanup(id,1); + history = await db.noteHistory.get(id); + expect(history.length).toBe(1); + let content = await db.noteHistory.content(history[0].sessionContentId); + expect(content.data).toBe(nextContent.data); + +}); + + + +test("Session should update if a sessionId is same", async () => { + let { db, id } = await noteTest(); + await sessionTest(db, id); + let content = { + data: await db.notes.note(id).content(), + type: "tiny", + }; + let note = db.notes.note(id).data; + let nextContent = { + data: content.data + "teststring", + type: "tiny", + }; + + await db.notes.add({ + id: id, + content: nextContent, + }); + let session = await db.noteHistory.add( + id, + note.dateEdited, + nextContent + ); + let history = await db.noteHistory.get(id); + expect(history.length).toBe(1); + + let sessionContent = await db.noteHistory.content(session.sessionContentId); + expect(sessionContent.data).toBe(nextContent.data); +}); + +test("History of note should be restored", async () => { + let { db, id } = await noteTest(); + let session = await sessionTest(db, id); + let prevContent = { + data: await db.notes.note(id).content(), + type: "tiny", + }; + + await db.notes.add({ + id: id, + content: { + data: "
", + type: "tiny", + }, + }); + await db.noteHistory.restore(session.id); + let nextContent = await db.notes.note(id).content(); + expect(nextContent).toBe(prevContent.data); +}); + +test("Session should not be created if values are falsy", async () => { + let db = await databaseTest(); + let session = await db.noteHistory.add(null, null, null); + expect(session).toBeFalsy(); +}); + +test("Should return empty array if no history available", async () => { + let { db, id } = await noteTest(); + let history = await db.noteHistory.get(id); + expect(history.length).toBe(0); +}); + +test("Session history of a given sessionId should be removed", async () => { + let { db, id } = await noteTest(); + let session = await sessionTest(db, id); + await db.noteHistory.removeSession(session.id); + let history = await db.noteHistory.get(id); + expect(history.length).toBe(0); +}); + +test("All sessions of a note should be cleared", async () => { + let { db, id } = await noteTest(); + await sessionTest(db, id); + + await db.noteHistory.clearSessions(id); + + let history = await db.noteHistory.get(id); + expect(history.length).toBe(0); +}); + +test("Sessions should be serialized and deserialized", async () => { + let { db, id } = await noteTest(); + await sessionTest(db, id); + let json = await db.noteHistory.serialize(); + await db.noteHistory.clearSessions(id); + await db.noteHistory.deserialize(json); + + let history = await db.noteHistory.get(id); + expect(history.length).toBe(1); + let content = await db.noteHistory.content(history[0].id); + expect(content).toBeTruthy(); +}); + +test("String should compress and decompress", () => { + let compressed = compress(TEST_NOTE.content.data); + let decompressed = decompress(compressed); + expect(decompressed).toBe(TEST_NOTE.content.data); +}); diff --git a/packages/core/api/index.js b/packages/core/api/index.js index 2678501a1..811863a96 100644 --- a/packages/core/api/index.js +++ b/packages/core/api/index.js @@ -23,6 +23,7 @@ import Offers from "./offers"; import Attachments from "../collections/attachments"; import Debug from "./debug"; import { Mutex } from "async-mutex"; +import NoteHistory from "../collections/note-history"; /** * @type {EventSource} @@ -109,6 +110,8 @@ class Database { this.content = await Content.new(this, "content", false); /** @type {Attachments} */ this.attachments = await Attachments.new(this, "attachments"); + /**@type {NoteHistory} */ + this.noteHistory = await NoteHistory.new(this,"notehistory"); this.trash = new Trash(this); diff --git a/packages/core/collections/contenthistory.js b/packages/core/collections/contenthistory.js new file mode 100644 index 000000000..a4038feaa --- /dev/null +++ b/packages/core/collections/contenthistory.js @@ -0,0 +1,54 @@ +import { compress, decompress } from "../utils/compression"; +import { makeSessionContentId } from "../utils/id"; +import Collection from "./collection"; + +export default class ContentHistory extends Collection { + /** + * + * @param {string} sessionId + * @param {{content:string:data:string}} content + */ + async add(sessionId, content, locked) { + if (!sessionId || !content) return; + let compressed = locked ? null : compress(content.data); + + await this._collection.addItem({ + id: makeSessionContentId(sessionId), + data: compressed || content.data, + type: content.type, + compressed: !!compressed, + locked + }); + } + + /** + * + * @param {string} sessionId + * @returns {Promise<{content:string;data:string}>} + */ + async get(sessionContentId) { + if (!sessionContentId) return; + let session = await this._collection.getItem(sessionContentId); + return { + data: session.compressed ? decompress(session.data) : session.data, + type: session.type, + }; + } + + /** + * + * @param {string} sessionContentId + */ + async remove(sessionContentId) { + await this._collection.deleteItem(sessionContentId); + } + + + async all() { + let indices = await this._collection.indexer.getIndices(); + let items = + (await this._collection.getItems(indices)); + + return Object.values(items); + } +} diff --git a/packages/core/collections/note-history.js b/packages/core/collections/note-history.js new file mode 100644 index 000000000..c032d1e17 --- /dev/null +++ b/packages/core/collections/note-history.js @@ -0,0 +1,213 @@ +import { makeSessionContentId, makeSessionId } from "../utils/id"; +import Collection from "./collection"; +import ContentHistory from "./contenthistory"; +/** + * @typedef Session + * @property {string} id + * @property {string} noteId + * @property {string} sessionContentId + * @property {string} dateEdited + * @property {string} dateCreated + * @property {boolean} locked + */ + +/** + * @typedef Content + * @property {string} data + * @property {string} type + */ + +export default class NoteHistory extends Collection { + constructor(db, name, cached) { + super(db, name, cached); + this.versionsLimit = 100; + } + + async init() { + super.init(); + + /** + * @type {ContentHistory} + */ + this.contentHistory = await ContentHistory.new( + this._db, + "contenthistory", + false + ); + } + + /** + * Get complete session history of a note. + * @param noteId id of the note + * @returns {Promise