undelete note WIP

This commit is contained in:
zadam 2020-01-03 10:48:36 +01:00
parent 1d3608b7bf
commit c1d0a1e07b
13 changed files with 217 additions and 75 deletions

View File

@ -0,0 +1,81 @@
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`contentLength` INT NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
INSERT INTO notes_mig (noteId, title, contentLength, isProtected, type, mime, hash, isDeleted, isErased, dateCreated, dateModified, utcDateCreated, utcDateModified)
SELECT noteId, title, -1, isProtected, type, mime, hash, isDeleted, isErased, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes;
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "branches_mig" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` INTEGER NOT NULL DEFAULT 0,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
INSERT INTO branches_mig (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, utcDateModified, utcDateCreated, hash)
SELECT branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, utcDateModified, utcDateCreated, hash FROM branches;
DROP TABLE branches;
ALTER TABLE branches_mig RENAME TO branches;
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
`deleteId` TEXT DEFAULT NULL,
hash TEXT default "" not null,
isInheritable int DEFAULT 0 NULL);
INSERT INTO attributes_mig (attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, hash, isInheritable)
SELECT attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, hash, isInheritable FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);

View File

@ -27,19 +27,6 @@ CREATE TABLE IF NOT EXISTS "options"
utcDateCreated TEXT not null,
utcDateModified TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "attributes"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
hash TEXT default "" not null,
isInheritable int DEFAULT 0 NULL);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
@ -47,12 +34,6 @@ CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
CREATE INDEX `IDX_sync_utcSyncDate` ON `sync` (
`utcSyncDate`
);
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);
CREATE TABLE IF NOT EXISTS "note_contents" (
`noteId` TEXT NOT NULL,
`content` TEXT NULL DEFAULT NULL,
@ -68,46 +49,10 @@ CREATE TABLE recent_notes
utcDateCreated TEXT not null,
isDeleted INT
);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` INTEGER NOT NULL DEFAULT 0,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`content` TEXT,
hash TEXT DEFAULT '' NOT NULL,
`utcDateModified` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`contentLength` INT NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
`title` TEXT,
@ -127,3 +72,61 @@ CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCr
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`contentLength` INT NOT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`isErased` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
`utcDateCreated` TEXT NOT NULL,
`utcDateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`));
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` INTEGER NOT NULL DEFAULT 0,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`deleteId` TEXT DEFAULT NULL,
`utcDateModified` TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`branchId`));
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE TABLE IF NOT EXISTS "attributes"
(
attributeId TEXT not null primary key,
noteId TEXT not null,
type TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
utcDateCreated TEXT not null,
utcDateModified TEXT not null,
isDeleted INT not null,
`deleteId` TEXT DEFAULT NULL,
hash TEXT default "" not null,
isInheritable int DEFAULT 0 NULL);
CREATE INDEX IDX_attributes_name_value
on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index
on attributes (noteId);
CREATE INDEX IDX_attributes_value_index
on attributes (value);

View File

@ -16,6 +16,7 @@ const sql = require('../services/sql');
* @property {int} position
* @property {boolean} isInheritable
* @property {boolean} isDeleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {string} utcDateCreated
* @property {string} utcDateModified
*

View File

@ -16,6 +16,7 @@ const sql = require('../services/sql');
* @property {string} prefix
* @property {boolean} isExpanded
* @property {boolean} isDeleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {string} utcDateModified
* @property {string} utcDateCreated
*
@ -25,7 +26,7 @@ class Branch extends Entity {
static get entityName() { return "branches"; }
static get primaryKeyName() { return "branchId"; }
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "prefix"]; }
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "deleteId", "prefix"]; }
constructor(row = {}) {
super(row);

View File

@ -24,6 +24,7 @@ const RELATION_DEFINITION = 'relation-definition';
* @property {int} contentLength - length of content
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
* @property {string|null} deleteId - ID identifying delete transaction
* @property {boolean} isErased - true if note's content is erased after it has been deleted
* @property {string} dateCreated - local date time (with offset)
* @property {string} dateModified - local date time (with offset)
@ -35,7 +36,7 @@ const RELATION_DEFINITION = 'relation-definition';
class Note extends Entity {
static get entityName() { return "notes"; }
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "title", "type", "isProtected", "isDeleted"]; }
static get hashedProperties() { return ["noteId", "title", "type", "isProtected", "isDeleted", "deleteId"]; }
/**
* @param row - object containing database row from "notes" table

View File

@ -28,31 +28,38 @@ export async function showDialog() {
const groupedByDate = groupByDate(result);
for (const [dateDay, dayChanges] of groupedByDate) {
const changesListEl = $('<ul>');
const $changesList = $('<ul>');
const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append(changesListEl);
const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append($changesList);
for (const change of dayChanges) {
const formattedTime = utils.formatTime(utils.parseDate(change.date));
let noteLink;
let $noteLink;
if (change.current_isDeleted) {
noteLink = change.current_title;
$noteLink = $("<span>").text(change.current_title);
if (change.canBeUndeleted) {
$noteLink
.append(' (')
.append($(`<a href="">`).text("undelete"))
.append(')');
}
}
else {
const note = await treeCache.getNote(change.noteId);
const notePath = await treeService.getSomeNotePath(note);
noteLink = await linkService.createNoteLink(notePath, {
$noteLink = await linkService.createNoteLink(notePath, {
title: change.title,
showNotePath: true
});
}
changesListEl.append($('<li>')
$changesList.append($('<li>')
.append(formattedTime + ' - ')
.append(noteLink));
.append($noteLink));
}
$content.append(dayEl);
@ -85,5 +92,6 @@ function groupByDate(result) {
groupedByDate.get(dateDay).push(row);
}
return groupedByDate;
}

View File

@ -10,6 +10,12 @@ function getNotePathFromUrl(url) {
}
async function createNoteLink(notePath, options = {}) {
if (!notePath || !notePath.trim()) {
console.error("Missing note path");
return $("<span>").text("[missing note]");
}
let noteTitle = options.title;
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;

View File

@ -316,6 +316,8 @@ async function getSomeNotePath(note) {
cur = parents[0];
}
path.push('root');
return path.reverse().join('/');
}

View File

@ -101,7 +101,8 @@ async function deleteBranch(req) {
const branch = await repository.getBranch(req.params.branchId);
const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes');
const noteDeleted = await notes.deleteBranch(branch, taskContext);
const deleteId = utils.randomString(10);
const noteDeleted = await notes.deleteBranch(branch, deleteId, taskContext);
if (last) {
taskContext.taskSucceeded();

View File

@ -55,12 +55,15 @@ async function deleteNote(req) {
const taskId = req.query.taskId;
const last = req.query.last === 'true';
// note how deleteId is separate from taskId - single taskId produces separate deleteId for each "top level" deleted note
const deleteId = utils.randomString(10);
const note = await repository.getNote(noteId);
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
for (const branch of await note.getBranches()) {
await noteService.deleteBranch(branch, taskContext);
await noteService.deleteBranch(branch, deleteId, taskContext);
}
if (last) {

View File

@ -10,6 +10,8 @@ async function getRecentChanges() {
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
note_revisions.title,
@ -19,21 +21,23 @@ async function getRecentChanges() {
JOIN notes USING(noteId)
ORDER BY
note_revisions.utcDateCreated DESC
LIMIT 1000
LIMIT 200
)
UNION ALL SELECT * FROM (
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateCreated AS date
notes.utcDateModified AS date
FROM
notes
ORDER BY
utcDateCreated DESC
LIMIT 1000
utcDateModified DESC
LIMIT 200
)
ORDER BY date DESC
LIMIT 200`);
@ -48,6 +52,27 @@ async function getRecentChanges() {
change.title = change.current_title = "[Protected]";
}
}
if (change.current_isDeleted) {
if (change.current_isErased) {
change.canBeUndeleted = false;
}
else {
const deleteId = change.current_deleteId;
const undeletedParentCount = await sql.getValue(`
SELECT COUNT(parentNote.noteId)
FROM branches
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
WHERE branches.noteId = ?
AND branches.isDeleted = 1
AND branches.deleteId = ?
AND parentNote.isDeleted = 0`, [change.noteId, deleteId]);
// note (and the subtree) can be undeleted if there's at least one undeleted parent (whose branch would be undeleted by this op)
change.canBeUndeleted = undeletedParentCount > 0;
}
}
}
return recentChanges;

View File

@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 155;
const SYNC_VERSION = 13;
const APP_DB_VERSION = 156;
const SYNC_VERSION = 14;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = {

View File

@ -389,8 +389,14 @@ async function updateNote(noteId, noteUpdates) {
};
}
/** @return {boolean} - true if note has been deleted, false otherwise */
async function deleteBranch(branch, taskContext) {
/**
* @param {Branch} branch
* @param {string} deleteId
* @param {TaskContext} taskContext
*
* @return {boolean} - true if note has been deleted, false otherwise
*/
async function deleteBranch(branch, deleteId, taskContext) {
taskContext.increaseProgressCount();
if (!branch || branch.isDeleted) {
@ -405,6 +411,7 @@ async function deleteBranch(branch, taskContext) {
}
branch.isDeleted = true;
branch.deleteId = deleteId;
await branch.save();
const note = await branch.getNote();
@ -412,19 +419,22 @@ async function deleteBranch(branch, taskContext) {
if (notDeletedBranches.length === 0) {
note.isDeleted = true;
note.deleteId = deleteId;
await note.save();
for (const childBranch of await note.getChildBranches()) {
await deleteBranch(childBranch, taskContext);
await deleteBranch(childBranch, deleteId, taskContext);
}
for (const attribute of await note.getOwnedAttributes()) {
attribute.isDeleted = true;
attribute.deleteId = deleteId;
await attribute.save();
}
for (const relation of await note.getTargetRelations()) {
relation.isDeleted = true;
relation.deleteId = deleteId;
await relation.save();
}