2017-12-14 22:16:26 -05:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const sql = require('./sql');
|
2018-04-02 21:25:20 -04:00
|
|
|
const sqlInit = require('./sql_init');
|
2017-12-14 22:16:26 -05:00
|
|
|
const log = require('./log');
|
2021-06-29 22:15:57 +02:00
|
|
|
const ws = require('./ws');
|
2018-04-01 21:27:46 -04:00
|
|
|
const syncMutexService = require('./sync_mutex');
|
2018-03-28 23:41:22 -04:00
|
|
|
const cls = require('./cls');
|
2021-06-29 22:15:57 +02:00
|
|
|
const entityChangesService = require('./entity_changes');
|
2019-11-10 11:25:41 +01:00
|
|
|
const optionsService = require('./options');
|
2023-01-03 13:52:37 +01:00
|
|
|
const BBranch = require('../becca/entities/bbranch');
|
2023-06-29 22:10:13 +02:00
|
|
|
const revisionService = require('./revisions');
|
2021-06-29 22:15:57 +02:00
|
|
|
const becca = require("../becca/becca");
|
2021-06-29 23:45:45 +02:00
|
|
|
const utils = require("../services/utils");
|
2022-12-27 10:22:50 +01:00
|
|
|
const {sanitizeAttributeName} = require("./sanitize_attribute_name");
|
2022-12-16 16:00:49 +01:00
|
|
|
const noteTypes = require("../services/note_types").getNoteTypeNames();
|
2017-12-14 22:16:26 -05:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
class ConsistencyChecks {
|
|
|
|
constructor(autoFix) {
|
|
|
|
this.autoFix = autoFix;
|
|
|
|
this.unrecoveredConsistencyErrors = false;
|
|
|
|
this.fixedIssues = false;
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = false;
|
2017-12-14 22:16:26 -05:00
|
|
|
}
|
2019-02-01 22:48:51 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
findAndFixIssues(query, fixerCb) {
|
|
|
|
const results = sql.getRows(query);
|
2019-02-01 22:48:51 +01:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
for (const res of results) {
|
|
|
|
try {
|
2020-07-01 22:42:59 +02:00
|
|
|
sql.transactional(() => fixerCb(res));
|
2018-01-01 19:41:22 -05:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
|
|
|
this.fixedIssues = true;
|
|
|
|
} else {
|
|
|
|
this.unrecoveredConsistencyErrors = true;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
logError(`Fixer failed with ${e.message} ${e.stack}`);
|
|
|
|
this.unrecoveredConsistencyErrors = true;
|
2018-01-01 19:41:22 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
return results;
|
2018-01-01 19:41:22 -05:00
|
|
|
}
|
2018-10-21 21:37:34 +02:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
checkTreeCycles() {
|
2019-12-10 22:03:00 +01:00
|
|
|
const childToParents = {};
|
2020-06-20 12:31:38 +02:00
|
|
|
const rows = sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0");
|
2018-01-01 19:41:22 -05:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
for (const row of rows) {
|
|
|
|
const childNoteId = row.noteId;
|
|
|
|
const parentNoteId = row.parentNoteId;
|
|
|
|
|
|
|
|
childToParents[childNoteId] = childToParents[childNoteId] || [];
|
|
|
|
childToParents[childNoteId].push(parentNoteId);
|
2019-11-10 14:16:12 +01:00
|
|
|
}
|
2019-01-21 22:51:49 +01:00
|
|
|
|
2022-05-03 00:30:09 +02:00
|
|
|
/** @returns {boolean} true if cycle was found and we should try again */
|
2020-03-11 22:43:20 +01:00
|
|
|
const checkTreeCycle = (noteId, path) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (noteId === 'root') {
|
2022-05-03 00:30:09 +02:00
|
|
|
return false;
|
2019-11-10 14:16:12 +01:00
|
|
|
}
|
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
for (const parentNoteId of childToParents[noteId]) {
|
|
|
|
if (path.includes(parentNoteId)) {
|
2022-05-03 00:30:09 +02:00
|
|
|
if (this.autoFix) {
|
|
|
|
const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId);
|
|
|
|
branch.markAsDeleted('cycle-autofix');
|
|
|
|
logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`);
|
2019-02-02 12:41:20 +01:00
|
|
|
|
2022-05-03 00:30:09 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Tree cycle detected at parent-child relationship: '${parentNoteId}' - '${noteId}', whole path: '${path}'`);
|
2022-05-03 00:30:09 +02:00
|
|
|
|
|
|
|
this.unrecoveredConsistencyErrors = true;
|
|
|
|
}
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
|
|
|
const newPath = path.slice();
|
|
|
|
newPath.push(noteId);
|
2019-11-10 11:43:33 +01:00
|
|
|
|
2022-05-03 00:30:09 +02:00
|
|
|
const retryNeeded = checkTreeCycle(parentNoteId, newPath);
|
|
|
|
|
|
|
|
if (retryNeeded) {
|
|
|
|
return true;
|
|
|
|
}
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2019-11-10 11:43:33 +01:00
|
|
|
}
|
2022-05-03 00:30:09 +02:00
|
|
|
|
|
|
|
return false;
|
2020-03-11 22:43:20 +01:00
|
|
|
};
|
2019-02-02 10:38:33 +01:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
const noteIds = Object.keys(childToParents);
|
2019-11-10 14:16:12 +01:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
for (const noteId of noteIds) {
|
2022-05-03 00:30:09 +02:00
|
|
|
const retryNeeded = checkTreeCycle(noteId, []);
|
|
|
|
|
|
|
|
if (retryNeeded) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
checkAndRepairTreeCycles() {
|
|
|
|
let treeFixed = false;
|
|
|
|
|
|
|
|
while (this.checkTreeCycles()) {
|
|
|
|
// fixing cycle means deleting branches, we might need to create a new branch to recover the note
|
|
|
|
this.findExistencyIssues();
|
|
|
|
|
|
|
|
treeFixed = true;
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2019-11-23 19:56:52 +01:00
|
|
|
|
2022-05-03 00:30:09 +02:00
|
|
|
if (treeFixed) {
|
|
|
|
this.reloadNeeded = true;
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
}
|
2018-11-19 23:11:36 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
findBrokenReferenceIssues() {
|
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT branchId, branches.noteId
|
|
|
|
FROM branches
|
2020-07-01 22:42:59 +02:00
|
|
|
LEFT JOIN notes USING (noteId)
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE branches.isDeleted = 0
|
|
|
|
AND notes.noteId IS NULL`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({branchId, noteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const branch = becca.getBranch(branchId);
|
2021-05-02 20:32:50 +02:00
|
|
|
branch.markAsDeleted();
|
2019-11-11 23:26:46 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Branch '${branchId}' has been deleted since it references missing note '${noteId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Branch '${branchId}' references missing note '${noteId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2022-11-06 15:18:32 +01:00
|
|
|
SELECT branchId, branches.parentNoteId AS parentNoteId
|
2019-12-10 22:03:00 +01:00
|
|
|
FROM branches
|
2020-07-01 22:42:59 +02:00
|
|
|
LEFT JOIN notes ON notes.noteId = branches.parentNoteId
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE branches.isDeleted = 0
|
2022-12-27 14:44:28 +01:00
|
|
|
AND branches.noteId != 'root'
|
2019-12-10 22:03:00 +01:00
|
|
|
AND notes.noteId IS NULL`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({branchId, parentNoteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2023-02-05 01:12:13 +02:00
|
|
|
// Delete the old branch and recreate it with root as parent.
|
|
|
|
const oldBranch = becca.getBranch(branchId);
|
|
|
|
const noteId = oldBranch.noteId;
|
|
|
|
oldBranch.markAsDeleted("missing-parent");
|
|
|
|
|
2023-06-29 22:10:13 +02:00
|
|
|
let message = `Branch '${branchId}' was missing parent note '${parentNoteId}', so it was deleted. `;
|
2023-02-10 10:09:06 +01:00
|
|
|
|
|
|
|
if (becca.getNote(noteId).getParentBranches().length === 0) {
|
2023-03-16 13:30:33 +01:00
|
|
|
const newBranch = new BBranch({
|
2023-02-10 10:09:06 +01:00
|
|
|
parentNoteId: 'root',
|
|
|
|
noteId: noteId,
|
|
|
|
prefix: 'recovered'
|
|
|
|
}).save();
|
|
|
|
|
|
|
|
message += `${newBranch.branchId} was created in the root instead.`;
|
|
|
|
} else {
|
|
|
|
message += `There is one or more valid branches, so no new one will be created as a replacement.`;
|
|
|
|
}
|
2019-11-23 19:56:52 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2023-02-10 10:09:06 +01:00
|
|
|
logFix(message);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Branch '${branchId}' references missing parent note '${parentNoteId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT attributeId, attributes.noteId
|
|
|
|
FROM attributes
|
2020-07-01 22:42:59 +02:00
|
|
|
LEFT JOIN notes USING (noteId)
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE attributes.isDeleted = 0
|
|
|
|
AND notes.noteId IS NULL`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({attributeId, noteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const attribute = becca.getAttribute(attributeId);
|
2021-05-02 20:32:50 +02:00
|
|
|
attribute.markAsDeleted();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Attribute '${attributeId}' has been deleted since it references missing source note '${noteId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Attribute '${attributeId}' references missing source note '${noteId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT attributeId, attributes.value AS noteId
|
|
|
|
FROM attributes
|
2020-07-01 22:42:59 +02:00
|
|
|
LEFT JOIN notes ON notes.noteId = attributes.value
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE attributes.isDeleted = 0
|
|
|
|
AND attributes.type = 'relation'
|
|
|
|
AND notes.noteId IS NULL`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({attributeId, noteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const attribute = becca.getAttribute(attributeId);
|
2021-05-02 20:32:50 +02:00
|
|
|
attribute.markAsDeleted();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Relation '${attributeId}' has been deleted since it references missing note '${noteId}'`)
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Relation '${attributeId}' references missing note '${noteId}'`)
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
2023-03-08 09:01:23 +01:00
|
|
|
|
2023-03-16 13:29:11 +01:00
|
|
|
this.findAndFixIssues(`
|
2023-07-14 17:01:56 +02:00
|
|
|
SELECT attachmentId, attachments.ownerId AS noteId
|
2023-03-16 13:29:11 +01:00
|
|
|
FROM attachments
|
2023-07-14 17:01:56 +02:00
|
|
|
WHERE attachments.ownerId NOT IN (
|
2023-03-16 13:29:11 +01:00
|
|
|
SELECT noteId FROM notes
|
|
|
|
UNION ALL
|
2023-06-04 23:01:40 +02:00
|
|
|
SELECT revisionId FROM revisions
|
2023-03-16 13:29:11 +01:00
|
|
|
)
|
|
|
|
AND attachments.isDeleted = 0`,
|
2023-07-14 17:01:56 +02:00
|
|
|
({attachmentId, ownerId}) => {
|
2023-03-16 13:29:11 +01:00
|
|
|
if (this.autoFix) {
|
|
|
|
const attachment = becca.getAttachment(attachmentId);
|
|
|
|
attachment.markAsDeleted();
|
|
|
|
|
|
|
|
this.reloadNeeded = false;
|
|
|
|
|
2023-07-14 17:01:56 +02:00
|
|
|
logFix(`Attachment '${attachmentId}' has been deleted since it references missing note/revision '${ownerId}'`);
|
2023-03-16 13:29:11 +01:00
|
|
|
} else {
|
2023-07-14 17:01:56 +02:00
|
|
|
logError(`Attachment '${attachmentId}' references missing note/revision '${ownerId}'`);
|
2023-03-16 13:29:11 +01:00
|
|
|
}
|
|
|
|
});
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2019-11-10 14:16:12 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
findExistencyIssues() {
|
2023-05-05 23:41:11 +02:00
|
|
|
// the principle for fixing inconsistencies is that if the note itself is deleted (isDeleted=true) then all related
|
|
|
|
// entities should be also deleted (branches, attributes), but if the note is not deleted,
|
2023-01-23 16:57:28 +01:00
|
|
|
// then at least one branch should exist.
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2023-05-05 23:41:11 +02:00
|
|
|
// the order here is important - first we might need to delete inconsistent branches, and after that
|
2019-12-10 22:03:00 +01:00
|
|
|
// another check might create missing branch
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT branchId,
|
|
|
|
noteId
|
|
|
|
FROM branches
|
2020-07-01 22:42:59 +02:00
|
|
|
JOIN notes USING (noteId)
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE notes.isDeleted = 1
|
|
|
|
AND branches.isDeleted = 0`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({branchId, noteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const branch = becca.getBranch(branchId);
|
2021-05-02 20:32:50 +02:00
|
|
|
branch.markAsDeleted();
|
2019-11-10 14:16:12 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2023-06-29 22:10:13 +02:00
|
|
|
logFix(`Branch '${branchId}' has been deleted since the associated note '${noteId}' is deleted.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2023-06-29 22:10:13 +02:00
|
|
|
logError(`Branch '${branchId}' is not deleted even though the associated note '${noteId}' is deleted.`)
|
2019-11-10 14:16:12 +01:00
|
|
|
}
|
2019-12-10 22:03:00 +01:00
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT branchId,
|
|
|
|
parentNoteId
|
|
|
|
FROM branches
|
2020-07-01 22:42:59 +02:00
|
|
|
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE parentNote.isDeleted = 1
|
|
|
|
AND branches.isDeleted = 0
|
2020-06-20 12:31:38 +02:00
|
|
|
`, ({branchId, parentNoteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const branch = becca.getBranch(branchId);
|
2021-05-02 20:32:50 +02:00
|
|
|
branch.markAsDeleted();
|
2018-03-13 19:18:52 -04:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2023-06-29 22:10:13 +02:00
|
|
|
logFix(`Branch '${branchId}' has been deleted since the associated parent note '${parentNoteId}' is deleted.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2023-06-29 22:10:13 +02:00
|
|
|
logError(`Branch '${branchId}' is not deleted even though the associated parent note '${parentNoteId}' is deleted.`)
|
2019-11-10 11:43:33 +01:00
|
|
|
}
|
2019-02-02 11:26:27 +01:00
|
|
|
});
|
2018-11-15 13:58:14 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT DISTINCT notes.noteId
|
|
|
|
FROM notes
|
2020-07-01 22:42:59 +02:00
|
|
|
LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE notes.isDeleted = 0
|
|
|
|
AND branches.branchId IS NULL
|
2020-06-20 12:31:38 +02:00
|
|
|
`, ({noteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2023-01-03 13:52:37 +01:00
|
|
|
const branch = new BBranch({
|
2019-12-10 22:03:00 +01:00
|
|
|
parentNoteId: 'root',
|
|
|
|
noteId: noteId,
|
|
|
|
prefix: 'recovered'
|
|
|
|
}).save();
|
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Created missing branch '${branch.branchId}' for note '${noteId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`No undeleted branch found for note '${noteId}'`);
|
2019-11-10 14:16:12 +01:00
|
|
|
}
|
|
|
|
});
|
2018-11-15 13:58:14 +01:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
// there should be a unique relationship between note and its parent
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT noteId,
|
|
|
|
parentNoteId
|
|
|
|
FROM branches
|
|
|
|
WHERE branches.isDeleted = 0
|
|
|
|
GROUP BY branches.parentNoteId,
|
|
|
|
branches.noteId
|
|
|
|
HAVING COUNT(1) > 1`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({noteId, parentNoteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 19:59:16 +02:00
|
|
|
const branchIds = sql.getColumn(
|
|
|
|
`SELECT branchId
|
2019-12-10 22:03:00 +01:00
|
|
|
FROM branches
|
|
|
|
WHERE noteId = ?
|
|
|
|
and parentNoteId = ?
|
2021-12-21 13:22:13 +01:00
|
|
|
and isDeleted = 0
|
2021-12-27 13:37:51 +01:00
|
|
|
ORDER BY utcDateModified`, [noteId, parentNoteId]);
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-05-02 19:59:16 +02:00
|
|
|
const branches = branchIds.map(branchId => becca.getBranch(branchId));
|
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
// it's not necessarily "original" branch, it's just the only one which will survive
|
|
|
|
const origBranch = branches[0];
|
|
|
|
|
|
|
|
// delete all but the first branch
|
|
|
|
for (const branch of branches.slice(1)) {
|
2021-05-02 20:32:50 +02:00
|
|
|
branch.markAsDeleted();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2022-12-27 21:17:40 +01:00
|
|
|
logFix(`Removing branch '${branch.branchId}' since it's a parent-child duplicate of branch '${origBranch.branchId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2021-10-03 10:00:38 +02:00
|
|
|
|
|
|
|
this.reloadNeeded = true;
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Duplicate branches for note '${noteId}' and parent '${parentNoteId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
2023-03-08 09:01:23 +01:00
|
|
|
|
|
|
|
this.findAndFixIssues(`
|
2023-03-16 12:17:55 +01:00
|
|
|
SELECT attachmentId,
|
2023-07-14 17:01:56 +02:00
|
|
|
attachments.ownerId AS noteId
|
2023-03-16 12:17:55 +01:00
|
|
|
FROM attachments
|
2023-07-14 17:01:56 +02:00
|
|
|
JOIN notes ON notes.noteId = attachments.ownerId
|
2023-03-08 09:01:23 +01:00
|
|
|
WHERE notes.isDeleted = 1
|
2023-03-16 12:17:55 +01:00
|
|
|
AND attachments.isDeleted = 0`,
|
|
|
|
({attachmentId, noteId}) => {
|
2023-03-08 09:01:23 +01:00
|
|
|
if (this.autoFix) {
|
2023-03-16 12:17:55 +01:00
|
|
|
const attachment = becca.getAttachment(attachmentId);
|
|
|
|
attachment.markAsDeleted();
|
2023-03-08 09:01:23 +01:00
|
|
|
|
|
|
|
this.reloadNeeded = false;
|
|
|
|
|
2023-06-29 22:10:13 +02:00
|
|
|
logFix(`Attachment '${attachmentId}' has been deleted since the associated note '${noteId}' is deleted.`);
|
2023-03-08 09:01:23 +01:00
|
|
|
} else {
|
2023-06-29 22:10:13 +02:00
|
|
|
logError(`Attachment '${attachmentId}' is not deleted even though the associated note '${noteId}' is deleted.`)
|
2023-03-08 09:01:23 +01:00
|
|
|
}
|
|
|
|
});
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2018-11-15 13:58:14 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
findLogicIssues() {
|
2022-01-12 19:32:23 +01:00
|
|
|
const noteTypesStr = noteTypes.map(nt => `'${nt}'`).join(", ");
|
2022-01-31 21:25:18 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT noteId, type
|
|
|
|
FROM notes
|
|
|
|
WHERE isDeleted = 0
|
2022-01-12 19:32:23 +01:00
|
|
|
AND type NOT IN (${noteTypesStr})`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({noteId, type}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const note = becca.getNote(noteId);
|
2023-05-05 23:41:11 +02:00
|
|
|
note.type = 'file'; // file is a safe option to recover notes if the type is not known
|
2020-06-20 12:31:38 +02:00
|
|
|
note.save();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Note '${noteId}' type has been change to file since it had invalid type '${type}'`)
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Note '${noteId}' has invalid type '${type}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-03-16 13:29:11 +01:00
|
|
|
this.findAndFixIssues(`
|
|
|
|
SELECT notes.noteId, notes.isProtected, notes.type, notes.mime
|
|
|
|
FROM notes
|
|
|
|
LEFT JOIN blobs USING (blobId)
|
|
|
|
WHERE blobs.blobId IS NULL
|
|
|
|
AND notes.isDeleted = 0`,
|
|
|
|
({noteId, isProtected, type, mime}) => {
|
|
|
|
if (this.autoFix) {
|
|
|
|
// it might be possible that the blob is not available only because of the interrupted
|
2023-06-23 00:26:47 +08:00
|
|
|
// sync, and it will come later. It's therefore important to guarantee that this artificial
|
2023-03-16 13:29:11 +01:00
|
|
|
// record won't overwrite the real one coming from the sync.
|
|
|
|
const fakeDate = "2000-01-01 00:00:00Z";
|
|
|
|
|
|
|
|
const blankContent = getBlankContent(isProtected, type, mime);
|
|
|
|
const blobId = utils.hashedBlobId(blankContent);
|
|
|
|
const blobAlreadyExists = !!sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [blobId]);
|
|
|
|
|
|
|
|
if (!blobAlreadyExists) {
|
|
|
|
// manually creating row since this can also affect deleted notes
|
|
|
|
sql.upsert("blobs", "blobId", {
|
|
|
|
noteId: noteId,
|
|
|
|
content: blankContent,
|
|
|
|
utcDateModified: fakeDate,
|
|
|
|
dateModified: fakeDate
|
|
|
|
});
|
|
|
|
|
|
|
|
const hash = utils.hash(utils.randomString(10));
|
|
|
|
|
2023-07-29 23:25:02 +02:00
|
|
|
entityChangesService.putEntityChange({
|
2023-03-16 13:29:11 +01:00
|
|
|
entityName: 'blobs',
|
|
|
|
entityId: blobId,
|
|
|
|
hash: hash,
|
|
|
|
isErased: false,
|
|
|
|
utcDateChanged: fakeDate,
|
|
|
|
isSynced: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
sql.execute("UPDATE notes SET blobId = ? WHERE noteId = ?", [blobId, noteId]);
|
|
|
|
|
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
|
|
|
logFix(`Note '${noteId}' content was set to empty string since there was no corresponding row`);
|
|
|
|
} else {
|
|
|
|
logError(`Note '${noteId}' content row does not exist`);
|
|
|
|
}
|
|
|
|
});
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2022-05-31 14:09:46 +02:00
|
|
|
if (sqlInit.getDbSize() < 500000) {
|
|
|
|
// querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/zadam/trilium/issues/2887
|
|
|
|
|
2023-03-16 13:29:11 +01:00
|
|
|
this.findAndFixIssues(`
|
|
|
|
SELECT notes.noteId, notes.type, notes.mime
|
|
|
|
FROM notes
|
|
|
|
JOIN blobs USING (blobId)
|
|
|
|
WHERE isDeleted = 0
|
|
|
|
AND isProtected = 0
|
|
|
|
AND content IS NULL`,
|
|
|
|
({noteId, type, mime}) => {
|
|
|
|
if (this.autoFix) {
|
|
|
|
const note = becca.getNote(noteId);
|
|
|
|
const blankContent = getBlankContent(false, type, mime);
|
|
|
|
note.setContent(blankContent);
|
|
|
|
|
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
|
|
|
logFix(`Note '${noteId}' content was set to '${blankContent}' since it was null even though it is not deleted`);
|
|
|
|
} else {
|
|
|
|
logError(`Note '${noteId}' content is null even though it is not deleted`);
|
|
|
|
}
|
|
|
|
});
|
2022-05-31 14:09:46 +02:00
|
|
|
}
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2023-03-16 13:29:11 +01:00
|
|
|
this.findAndFixIssues(`
|
2023-06-04 23:01:40 +02:00
|
|
|
SELECT revisions.revisionId
|
|
|
|
FROM revisions
|
2023-03-16 13:29:11 +01:00
|
|
|
LEFT JOIN blobs USING (blobId)
|
|
|
|
WHERE blobs.blobId IS NULL`,
|
2023-06-04 23:01:40 +02:00
|
|
|
({revisionId}) => {
|
2023-03-16 13:29:11 +01:00
|
|
|
if (this.autoFix) {
|
2023-06-04 23:01:40 +02:00
|
|
|
revisionService.eraseRevisions([revisionId]);
|
2023-03-16 13:29:11 +01:00
|
|
|
|
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2023-06-04 23:01:40 +02:00
|
|
|
logFix(`Note revision content '${revisionId}' was set to erased since its content did not exist.`);
|
2023-03-16 13:29:11 +01:00
|
|
|
} else {
|
2023-06-04 23:01:40 +02:00
|
|
|
logError(`Note revision content '${revisionId}' does not exist`);
|
2023-03-16 13:29:11 +01:00
|
|
|
}
|
|
|
|
});
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT parentNoteId
|
|
|
|
FROM branches
|
2020-07-01 22:42:59 +02:00
|
|
|
JOIN notes ON notes.noteId = branches.parentNoteId
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE notes.isDeleted = 0
|
|
|
|
AND notes.type == 'search'
|
|
|
|
AND branches.isDeleted = 0`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({parentNoteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-09-12 11:18:06 +02:00
|
|
|
const branchIds = sql.getColumn(`
|
|
|
|
SELECT branchId
|
|
|
|
FROM branches
|
|
|
|
WHERE isDeleted = 0
|
|
|
|
AND parentNoteId = ?`, [parentNoteId]);
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-05-02 19:59:16 +02:00
|
|
|
const branches = branchIds.map(branchId => becca.getBranch(branchId));
|
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
for (const branch of branches) {
|
2023-02-10 10:12:58 +01:00
|
|
|
// delete the old wrong branch
|
|
|
|
branch.markAsDeleted("parent-is-search");
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2023-02-10 10:12:58 +01:00
|
|
|
// create a replacement branch in root parent
|
2023-03-16 13:29:11 +01:00
|
|
|
new BBranch({
|
2023-02-10 10:12:58 +01:00
|
|
|
parentNoteId: 'root',
|
|
|
|
noteId: branch.noteId,
|
|
|
|
prefix: 'recovered'
|
|
|
|
}).save();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2023-02-10 10:12:58 +01:00
|
|
|
logFix(`Note '${branch.noteId}' has been moved to root since it was a child of a search note '${parentNoteId}'`)
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2021-10-03 10:00:38 +02:00
|
|
|
|
|
|
|
this.reloadNeeded = true;
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Search note '${parentNoteId}' has children`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT attributeId
|
|
|
|
FROM attributes
|
|
|
|
WHERE isDeleted = 0
|
|
|
|
AND type = 'relation'
|
|
|
|
AND value = ''`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({attributeId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const relation = becca.getAttribute(attributeId);
|
2021-05-02 20:32:50 +02:00
|
|
|
relation.markAsDeleted();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Removed relation '${relation.attributeId}' of name '${relation.name}' with empty target.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Relation '${attributeId}' has empty target.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT attributeId,
|
|
|
|
type
|
|
|
|
FROM attributes
|
|
|
|
WHERE isDeleted = 0
|
|
|
|
AND type != 'label'
|
2020-09-08 20:42:50 +02:00
|
|
|
AND type != 'relation'`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({attributeId, type}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const attribute = becca.getAttribute(attributeId);
|
2019-12-10 22:03:00 +01:00
|
|
|
attribute.type = 'label';
|
2020-06-20 12:31:38 +02:00
|
|
|
attribute.save();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Attribute '${attributeId}' type was changed to label since it had invalid type '${type}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Attribute '${attributeId}' has invalid type '${type}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT attributeId,
|
|
|
|
attributes.noteId
|
|
|
|
FROM attributes
|
2021-09-12 11:18:06 +02:00
|
|
|
JOIN notes ON attributes.noteId = notes.noteId
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE attributes.isDeleted = 0
|
|
|
|
AND notes.isDeleted = 1`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({attributeId, noteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const attribute = becca.getAttribute(attributeId);
|
2021-05-02 20:32:50 +02:00
|
|
|
attribute.markAsDeleted();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Removed attribute '${attributeId}' because owning note '${noteId}' is also deleted.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Attribute '${attributeId}' is not deleted even though owning note '${noteId}' is deleted.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2019-12-10 22:03:00 +01:00
|
|
|
SELECT attributeId,
|
|
|
|
attributes.value AS targetNoteId
|
|
|
|
FROM attributes
|
2021-09-12 11:18:06 +02:00
|
|
|
JOIN notes ON attributes.value = notes.noteId
|
2019-12-10 22:03:00 +01:00
|
|
|
WHERE attributes.type = 'relation'
|
|
|
|
AND attributes.isDeleted = 0
|
|
|
|
AND notes.isDeleted = 1`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({attributeId, targetNoteId}) => {
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.autoFix) {
|
2021-05-02 11:23:58 +02:00
|
|
|
const attribute = becca.getAttribute(attributeId);
|
2021-05-02 20:32:50 +02:00
|
|
|
attribute.markAsDeleted();
|
2019-12-10 22:03:00 +01:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Removed attribute '${attributeId}' because target note '${targetNoteId}' is also deleted.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Attribute '${attributeId}' is not deleted even though target note '${targetNoteId}' is deleted.`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-02-01 22:48:51 +01:00
|
|
|
|
2020-08-02 23:43:39 +02:00
|
|
|
runEntityChangeChecks(entityName, key) {
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2023-07-29 21:59:20 +02:00
|
|
|
SELECT ${key} as entityId
|
|
|
|
FROM ${entityName}
|
|
|
|
LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key}
|
|
|
|
WHERE ec.id IS NULL`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({entityId}) => {
|
2023-06-05 09:23:42 +02:00
|
|
|
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
|
2021-08-07 21:21:30 +02:00
|
|
|
|
2021-09-08 21:54:26 +02:00
|
|
|
if (this.autoFix) {
|
2023-07-29 23:25:02 +02:00
|
|
|
entityChangesService.putEntityChange({
|
2021-09-08 21:54:26 +02:00
|
|
|
entityName,
|
|
|
|
entityId,
|
2023-05-05 23:41:11 +02:00
|
|
|
hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK
|
2023-07-29 21:59:20 +02:00
|
|
|
isErased: false,
|
2023-06-05 09:23:42 +02:00
|
|
|
utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated,
|
|
|
|
isSynced: entityName !== 'options' || entityRow.isSynced
|
2021-09-08 21:54:26 +02:00
|
|
|
});
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Missing entity change for entityName '${entityName}', entityId '${entityId}'`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
});
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findAndFixIssues(`
|
2023-07-29 21:59:20 +02:00
|
|
|
SELECT id, entityId
|
|
|
|
FROM entity_changes
|
|
|
|
LEFT JOIN ${entityName} ON entityId = ${entityName}.${key}
|
2020-12-14 13:15:32 +01:00
|
|
|
WHERE
|
|
|
|
entity_changes.isErased = 0
|
|
|
|
AND entity_changes.entityName = '${entityName}'
|
2023-07-29 21:59:20 +02:00
|
|
|
AND ${entityName}.${key} IS NULL`,
|
2020-06-20 12:31:38 +02:00
|
|
|
({id, entityId}) => {
|
2020-02-19 19:51:36 +01:00
|
|
|
if (this.autoFix) {
|
2020-08-02 23:27:48 +02:00
|
|
|
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
|
2020-02-19 19:51:36 +01:00
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Deleted extra entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
|
2020-02-19 19:51:36 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Unrecognized entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
|
2020-02-19 19:51:36 +01:00
|
|
|
}
|
|
|
|
});
|
2021-12-21 14:25:26 +01:00
|
|
|
|
|
|
|
this.findAndFixIssues(`
|
2023-07-29 21:59:20 +02:00
|
|
|
SELECT id, entityId
|
|
|
|
FROM entity_changes
|
|
|
|
JOIN ${entityName} ON entityId = ${entityName}.${key}
|
2021-12-21 14:25:26 +01:00
|
|
|
WHERE
|
|
|
|
entity_changes.isErased = 1
|
|
|
|
AND entity_changes.entityName = '${entityName}'`,
|
|
|
|
({id, entityId}) => {
|
|
|
|
if (this.autoFix) {
|
|
|
|
sql.execute(`DELETE FROM ${entityName} WHERE ${key} = ?`, [entityId]);
|
|
|
|
|
|
|
|
this.reloadNeeded = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Erasing entityName '${entityName}', entityId '${entityId}' since entity change id '${id}' has it as erased.`);
|
2021-12-21 14:25:26 +01:00
|
|
|
} else {
|
2022-12-27 10:22:50 +01:00
|
|
|
logError(`Entity change id '${id}' has entityName '${entityName}', entityId '${entityId}' as erased, but it's not.`);
|
2021-12-21 14:25:26 +01:00
|
|
|
}
|
|
|
|
});
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2020-08-02 23:43:39 +02:00
|
|
|
findEntityChangeIssues() {
|
|
|
|
this.runEntityChangeChecks("notes", "noteId");
|
2023-06-04 23:01:40 +02:00
|
|
|
this.runEntityChangeChecks("revisions", "revisionId");
|
2023-03-16 12:17:55 +01:00
|
|
|
this.runEntityChangeChecks("attachments", "attachmentId");
|
2023-03-16 11:02:07 +01:00
|
|
|
this.runEntityChangeChecks("blobs", "blobId");
|
2020-08-02 23:43:39 +02:00
|
|
|
this.runEntityChangeChecks("branches", "branchId");
|
|
|
|
this.runEntityChangeChecks("attributes", "attributeId");
|
2022-01-10 17:09:20 +01:00
|
|
|
this.runEntityChangeChecks("etapi_tokens", "etapiTokenId");
|
2020-08-02 23:43:39 +02:00
|
|
|
this.runEntityChangeChecks("options", "name");
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2020-07-04 11:02:05 +02:00
|
|
|
findWronglyNamedAttributes() {
|
|
|
|
const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`);
|
|
|
|
|
|
|
|
for (const origName of attrNames) {
|
2022-12-23 14:18:40 +01:00
|
|
|
const fixedName = sanitizeAttributeName(origName);
|
2020-07-04 11:02:05 +02:00
|
|
|
|
2020-11-17 22:35:20 +01:00
|
|
|
if (fixedName !== origName) {
|
2020-07-04 11:02:05 +02:00
|
|
|
if (this.autoFix) {
|
|
|
|
// there isn't a good way to update this:
|
|
|
|
// - just SQL query will fix it in DB but not notify frontend (or other caches) that it has been fixed
|
|
|
|
// - renaming the attribute would break the invariant that single attribute never changes the name
|
|
|
|
// - deleting the old attribute and creating new will create duplicates across synchronized cluster (specifically in the initial migration)
|
2023-05-05 23:41:11 +02:00
|
|
|
// But in general, we assume there won't be many such problems
|
2020-07-04 11:02:05 +02:00
|
|
|
sql.execute('UPDATE attributes SET name = ? WHERE name = ?', [fixedName, origName]);
|
|
|
|
|
|
|
|
this.fixedIssues = true;
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = true;
|
2020-07-04 11:02:05 +02:00
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`Renamed incorrectly named attributes '${origName}' to '${fixedName}'`);
|
2020-07-04 11:02:05 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.unrecoveredConsistencyErrors = true;
|
|
|
|
|
2022-12-27 10:22:50 +01:00
|
|
|
logFix(`There are incorrectly named attributes '${origName}'`);
|
2020-07-04 11:02:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-15 20:44:31 +01:00
|
|
|
findSyncIssues() {
|
|
|
|
const lastSyncedPush = parseInt(sql.getValue("SELECT value FROM options WHERE name = 'lastSyncedPush'"));
|
|
|
|
const maxEntityChangeId = sql.getValue("SELECT MAX(id) FROM entity_changes");
|
|
|
|
|
|
|
|
if (lastSyncedPush > maxEntityChangeId) {
|
|
|
|
if (this.autoFix) {
|
|
|
|
sql.execute("UPDATE options SET value = ? WHERE name = 'lastSyncedPush'", [maxEntityChangeId]);
|
|
|
|
|
|
|
|
this.fixedIssues = true;
|
|
|
|
|
|
|
|
logFix(`Fixed incorrect lastSyncedPush - was ${lastSyncedPush}, needs to be at maximum ${maxEntityChangeId}`);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.unrecoveredConsistencyErrors = true;
|
|
|
|
|
|
|
|
logFix(`Incorrect lastSyncedPush - is ${lastSyncedPush}, needs to be at maximum ${maxEntityChangeId}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-01 22:42:59 +02:00
|
|
|
runAllChecksAndFixers() {
|
2019-12-10 22:03:00 +01:00
|
|
|
this.unrecoveredConsistencyErrors = false;
|
|
|
|
this.fixedIssues = false;
|
2021-10-03 10:00:38 +02:00
|
|
|
this.reloadNeeded = false;
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2021-12-21 14:25:26 +01:00
|
|
|
this.findEntityChangeIssues();
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findBrokenReferenceIssues();
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findExistencyIssues();
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
this.findLogicIssues();
|
2019-02-02 11:26:27 +01:00
|
|
|
|
2020-07-04 11:02:05 +02:00
|
|
|
this.findWronglyNamedAttributes();
|
|
|
|
|
2021-02-15 20:44:31 +01:00
|
|
|
this.findSyncIssues();
|
|
|
|
|
2020-05-03 22:49:20 +02:00
|
|
|
// root branch should always be expanded
|
2022-12-27 14:44:28 +01:00
|
|
|
sql.execute("UPDATE branches SET isExpanded = 1 WHERE noteId = 'root'");
|
2020-05-03 22:49:20 +02:00
|
|
|
|
2020-12-09 22:49:55 +01:00
|
|
|
if (!this.unrecoveredConsistencyErrors) {
|
2019-12-10 22:03:00 +01:00
|
|
|
// we run this only if basic checks passed since this assumes basic data consistency
|
2018-01-01 19:41:22 -05:00
|
|
|
|
2022-05-03 00:30:09 +02:00
|
|
|
this.checkAndRepairTreeCycles();
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2018-01-01 19:41:22 -05:00
|
|
|
|
2021-10-03 10:00:38 +02:00
|
|
|
if (this.reloadNeeded) {
|
2021-09-12 11:18:06 +02:00
|
|
|
require("../becca/becca_loader").reload();
|
2021-05-01 11:38:20 +02:00
|
|
|
}
|
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
return !this.unrecoveredConsistencyErrors;
|
|
|
|
}
|
2018-01-04 21:37:36 -05:00
|
|
|
|
2020-12-16 15:01:20 +01:00
|
|
|
runDbDiagnostics() {
|
2020-12-25 13:06:58 +01:00
|
|
|
function getTableRowCount(tableName) {
|
2020-12-16 15:01:20 +01:00
|
|
|
const count = sql.getValue(`SELECT COUNT(1) FROM ${tableName}`);
|
2019-11-30 09:15:08 +01:00
|
|
|
|
2020-12-23 21:22:41 +01:00
|
|
|
return `${tableName}: ${count}`;
|
2020-12-16 15:01:20 +01:00
|
|
|
}
|
2019-11-30 09:15:08 +01:00
|
|
|
|
2023-06-04 23:01:40 +02:00
|
|
|
const tables = [ "notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens" ];
|
2020-12-23 21:22:41 +01:00
|
|
|
|
2022-12-21 15:19:05 +01:00
|
|
|
log.info(`Table counts: ${tables.map(tableName => getTableRowCount(tableName)).join(", ")}`);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2019-11-30 09:15:08 +01:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
async runChecks() {
|
|
|
|
let elapsedTimeMs;
|
2018-01-04 21:37:36 -05:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
await syncMutexService.doExclusively(() => {
|
2022-12-27 14:44:28 +01:00
|
|
|
elapsedTimeMs = this.runChecksInner();
|
2019-12-10 22:03:00 +01:00
|
|
|
});
|
2018-01-01 19:47:50 -05:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
if (this.unrecoveredConsistencyErrors) {
|
|
|
|
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms)`);
|
2018-01-01 19:41:22 -05:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
ws.sendMessageToAllClients({type: 'consistency-checks-failed'});
|
|
|
|
} else {
|
2021-09-30 22:26:56 +02:00
|
|
|
log.info(`All consistency checks passed ` +
|
|
|
|
(this.fixedIssues ? "after some fixes" : "with no errors detected") +
|
|
|
|
` (took ${elapsedTimeMs}ms)`
|
|
|
|
);
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
2017-12-14 23:30:38 -05:00
|
|
|
}
|
2022-12-27 14:44:28 +01:00
|
|
|
|
|
|
|
runChecksInner() {
|
|
|
|
const startTimeMs = Date.now();
|
|
|
|
|
|
|
|
this.runDbDiagnostics();
|
|
|
|
|
|
|
|
this.runAllChecksAndFixers();
|
|
|
|
|
|
|
|
return Date.now() - startTimeMs;
|
|
|
|
}
|
2017-12-14 22:16:26 -05:00
|
|
|
}
|
|
|
|
|
2021-09-11 14:34:37 +02:00
|
|
|
function getBlankContent(isProtected, type, mime) {
|
|
|
|
if (isProtected) {
|
2023-05-05 23:41:11 +02:00
|
|
|
return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without a password
|
2021-09-11 14:34:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (mime === 'application/json') {
|
|
|
|
return '{}';
|
|
|
|
}
|
|
|
|
|
2023-06-30 11:18:34 +02:00
|
|
|
return ''; // empty string might be a wrong choice for some note types, but it's the best guess
|
2021-09-11 14:34:37 +02:00
|
|
|
}
|
|
|
|
|
2019-02-02 09:26:57 +01:00
|
|
|
function logFix(message) {
|
2022-12-21 15:19:05 +01:00
|
|
|
log.info(`Consistency issue fixed: ${message}`);
|
2019-02-02 09:26:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function logError(message) {
|
2022-12-21 15:19:05 +01:00
|
|
|
log.info(`Consistency error: ${message}`);
|
2019-02-02 09:26:57 +01:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function runPeriodicChecks() {
|
|
|
|
const autoFix = optionsService.getOptionBool('autoFixConsistencyIssues');
|
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
const consistencyChecks = new ConsistencyChecks(autoFix);
|
2020-06-20 12:31:38 +02:00
|
|
|
consistencyChecks.runChecks();
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
|
2022-12-27 14:44:28 +01:00
|
|
|
async function runOnDemandChecks(autoFix) {
|
2019-12-10 22:03:00 +01:00
|
|
|
const consistencyChecks = new ConsistencyChecks(autoFix);
|
2022-12-27 14:44:28 +01:00
|
|
|
await consistencyChecks.runChecks();
|
|
|
|
}
|
|
|
|
|
|
|
|
function runOnDemandChecksWithoutExclusiveLock(autoFix) {
|
|
|
|
const consistencyChecks = new ConsistencyChecks(autoFix);
|
|
|
|
consistencyChecks.runChecksInner();
|
2019-12-10 22:03:00 +01:00
|
|
|
}
|
|
|
|
|
2021-11-17 22:57:09 +01:00
|
|
|
function runEntityChangesChecks() {
|
|
|
|
const consistencyChecks = new ConsistencyChecks(true);
|
|
|
|
consistencyChecks.findEntityChangeIssues();
|
|
|
|
}
|
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
sqlInit.dbReady.then(() => {
|
|
|
|
setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
|
2017-12-14 22:16:26 -05:00
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
// kickoff checks soon after startup (to not block the initial load)
|
2021-11-12 21:19:23 +01:00
|
|
|
setTimeout(cls.wrap(runPeriodicChecks), 4 * 1000);
|
2020-06-20 21:42:41 +02:00
|
|
|
});
|
2017-12-14 22:16:26 -05:00
|
|
|
|
2019-12-10 22:03:00 +01:00
|
|
|
module.exports = {
|
2021-11-17 22:57:09 +01:00
|
|
|
runOnDemandChecks,
|
2022-12-27 14:44:28 +01:00
|
|
|
runOnDemandChecksWithoutExclusiveLock,
|
2021-11-17 22:57:09 +01:00
|
|
|
runEntityChangesChecks
|
2020-06-20 12:31:38 +02:00
|
|
|
};
|