mirror of
https://github.com/zadam/trilium.git
synced 2025-11-16 18:25:51 +01:00
chore(prettier): fix all files
This commit is contained in:
@@ -9,9 +9,9 @@ import path from "path";
|
||||
function getFullAnonymizationScript() {
|
||||
// we want to delete all non-builtin attributes because they can contain sensitive names and values
|
||||
// on the other hand builtin/system attrs should not contain any sensitive info
|
||||
const builtinAttrNames = BUILTIN_ATTRIBUTES
|
||||
.filter(attr => !["shareCredentials", "shareAlias"].includes(attr.name))
|
||||
.map(attr => `'${attr.name}'`).join(', ');
|
||||
const builtinAttrNames = BUILTIN_ATTRIBUTES.filter((attr) => !["shareCredentials", "shareAlias"].includes(attr.name))
|
||||
.map((attr) => `'${attr.name}'`)
|
||||
.join(", ");
|
||||
|
||||
const anonymizeScript = `
|
||||
UPDATE etapi_tokens SET tokenHash = 'API token hash value';
|
||||
@@ -49,7 +49,7 @@ function getLightAnonymizationScript() {
|
||||
}
|
||||
|
||||
async function createAnonymizedCopy(type: "full" | "light") {
|
||||
if (!['full', 'light'].includes(type)) {
|
||||
if (!["full", "light"].includes(type)) {
|
||||
throw new Error(`Unrecognized anonymization type '${type}'`);
|
||||
}
|
||||
|
||||
@@ -63,9 +63,7 @@ async function createAnonymizedCopy(type: "full" | "light") {
|
||||
|
||||
const db = new Database(anonymizedFile);
|
||||
|
||||
const anonymizationScript = type === 'light'
|
||||
? getLightAnonymizationScript()
|
||||
: getFullAnonymizationScript();
|
||||
const anonymizationScript = type === "light" ? getLightAnonymizationScript() : getFullAnonymizationScript();
|
||||
|
||||
db.exec(anonymizationScript);
|
||||
|
||||
@@ -82,9 +80,10 @@ function getExistingAnonymizedDatabases() {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs.readdirSync(dataDir.ANONYMIZED_DB_DIR)
|
||||
.filter(fileName => fileName.includes("anonymized"))
|
||||
.map(fileName => ({
|
||||
return fs
|
||||
.readdirSync(dataDir.ANONYMIZED_DB_DIR)
|
||||
.filter((fileName) => fileName.includes("anonymized"))
|
||||
.map((fileName) => ({
|
||||
fileName: fileName,
|
||||
filePath: path.resolve(dataDir.ANONYMIZED_DB_DIR, fileName)
|
||||
}));
|
||||
@@ -94,4 +93,4 @@ export default {
|
||||
getFullAnonymizationScript,
|
||||
createAnonymizedCopy,
|
||||
getExistingAnonymizedDatabases
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,5 +13,5 @@ export interface SetupStatusResponse {
|
||||
*/
|
||||
export interface SetupSyncSeedResponse {
|
||||
syncVersion: number;
|
||||
options: OptionRow[]
|
||||
options: OptionRow[];
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ Terminal=false
|
||||
* We overwrite this file during every run as it might have been updated.
|
||||
*/
|
||||
function installLocalAppIcon() {
|
||||
if (!isElectron()
|
||||
|| ["win32", "darwin"].includes(os.platform())
|
||||
|| (config.General && config.General.noDesktopIcon)) {
|
||||
if (!isElectron() || ["win32", "darwin"].includes(os.platform()) || (config.General && config.General.noDesktopIcon)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +33,7 @@ function installLocalAppIcon() {
|
||||
return;
|
||||
}
|
||||
|
||||
const desktopDir = path.resolve(os.homedir(), '.local/share/applications');
|
||||
const desktopDir = path.resolve(os.homedir(), ".local/share/applications");
|
||||
|
||||
fs.stat(desktopDir, function (err, stats) {
|
||||
if (err) {
|
||||
@@ -56,9 +54,7 @@ function installLocalAppIcon() {
|
||||
}
|
||||
|
||||
function getDesktopFileContent() {
|
||||
return template
|
||||
.replace("#APP_ROOT_DIR#", escapePath(resourceDir.ELECTRON_APP_ROOT_DIR))
|
||||
.replace("#EXE_PATH#", escapePath(getExePath()));
|
||||
return template.replace("#APP_ROOT_DIR#", escapePath(resourceDir.ELECTRON_APP_ROOT_DIR)).replace("#EXE_PATH#", escapePath(getExePath()));
|
||||
}
|
||||
|
||||
function escapePath(path: string) {
|
||||
@@ -66,7 +62,7 @@ function escapePath(path: string) {
|
||||
}
|
||||
|
||||
function getExePath() {
|
||||
return path.resolve(resourceDir.ELECTRON_APP_ROOT_DIR, 'trilium');
|
||||
return path.resolve(resourceDir.ELECTRON_APP_ROOT_DIR, "trilium");
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import assetPath from "./asset_path.js";
|
||||
import env from "./env.js";
|
||||
|
||||
export default env.isDev()
|
||||
? assetPath + "/app"
|
||||
: assetPath + "/app-dist";
|
||||
export default env.isDev() ? assetPath + "/app" : assetPath + "/app-dist";
|
||||
|
||||
@@ -3,26 +3,24 @@
|
||||
import { AttributeRow } from "../becca/entities/rows.js";
|
||||
|
||||
function formatAttrForSearch(attr: AttributeRow, searchWithValue: boolean) {
|
||||
let searchStr = '';
|
||||
let searchStr = "";
|
||||
|
||||
if (attr.type === 'label') {
|
||||
searchStr += '#';
|
||||
}
|
||||
else if (attr.type === 'relation') {
|
||||
searchStr += '~';
|
||||
}
|
||||
else {
|
||||
if (attr.type === "label") {
|
||||
searchStr += "#";
|
||||
} else if (attr.type === "relation") {
|
||||
searchStr += "~";
|
||||
} else {
|
||||
throw new Error(`Unrecognized attribute type ${JSON.stringify(attr)}`);
|
||||
}
|
||||
|
||||
searchStr += attr.name;
|
||||
|
||||
if (searchWithValue && attr.value) {
|
||||
if (attr.type === 'relation') {
|
||||
if (attr.type === "relation") {
|
||||
searchStr += ".noteId";
|
||||
}
|
||||
|
||||
searchStr += '=';
|
||||
searchStr += "=";
|
||||
searchStr += formatValue(attr.value);
|
||||
}
|
||||
|
||||
@@ -32,17 +30,13 @@ function formatAttrForSearch(attr: AttributeRow, searchWithValue: boolean) {
|
||||
function formatValue(val: string) {
|
||||
if (!/[^\w]/.test(val)) {
|
||||
return val;
|
||||
}
|
||||
else if (!val.includes('"')) {
|
||||
} else if (!val.includes('"')) {
|
||||
return `"${val}"`;
|
||||
}
|
||||
else if (!val.includes("'")) {
|
||||
} else if (!val.includes("'")) {
|
||||
return `'${val}'`;
|
||||
}
|
||||
else if (!val.includes("`")) {
|
||||
} else if (!val.includes("`")) {
|
||||
return `\`${val}\``;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return `"${val.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import BAttribute from "../becca/entities/battribute.js";
|
||||
import attributeFormatter from "./attribute_formatter.js";
|
||||
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import { AttributeRow } from '../becca/entities/rows.js';
|
||||
import { AttributeRow } from "../becca/entities/rows.js";
|
||||
|
||||
const ATTRIBUTE_TYPES = new Set(['label', 'relation']);
|
||||
const ATTRIBUTE_TYPES = new Set(["label", "relation"]);
|
||||
|
||||
function getNotesWithLabel(name: string, value?: string): BNote[] {
|
||||
const query = attributeFormatter.formatAttrForSearch({type: 'label', name, value}, value !== undefined);
|
||||
const query = attributeFormatter.formatAttrForSearch({ type: "label", name, value }, value !== undefined);
|
||||
return searchService.searchNotes(query, {
|
||||
includeArchivedNotes: true,
|
||||
ignoreHoistedNote: true
|
||||
@@ -22,7 +22,7 @@ function getNotesWithLabel(name: string, value?: string): BNote[] {
|
||||
// TODO: should be in search service
|
||||
function getNoteWithLabel(name: string, value?: string): BNote | null {
|
||||
// optimized version (~20 times faster) without using normal search, useful for e.g., finding date notes
|
||||
const attrs = becca.findAttributes('label', name);
|
||||
const attrs = becca.findAttributes("label", name);
|
||||
|
||||
if (value === undefined) {
|
||||
return attrs[0]?.getNote();
|
||||
@@ -42,7 +42,7 @@ function getNoteWithLabel(name: string, value?: string): BNote | null {
|
||||
function createLabel(noteId: string, name: string, value: string = "") {
|
||||
return createAttribute({
|
||||
noteId: noteId,
|
||||
type: 'label',
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
@@ -51,7 +51,7 @@ function createLabel(noteId: string, name: string, value: string = "") {
|
||||
function createRelation(noteId: string, name: string, targetNoteId: string) {
|
||||
return createAttribute({
|
||||
noteId: noteId,
|
||||
type: 'relation',
|
||||
type: "relation",
|
||||
name: name,
|
||||
value: targetNoteId
|
||||
});
|
||||
@@ -69,7 +69,9 @@ function getAttributeNames(type: string, nameLike: string) {
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND type = ?
|
||||
AND name LIKE ?`, [type, `%${nameLike}%`]);
|
||||
AND name LIKE ?`,
|
||||
[type, `%${nameLike}%`]
|
||||
);
|
||||
|
||||
for (const attr of BUILTIN_ATTRIBUTES) {
|
||||
if (attr.type === type && attr.name.toLowerCase().includes(nameLike) && !names.includes(attr.name)) {
|
||||
@@ -77,12 +79,7 @@ function getAttributeNames(type: string, nameLike: string) {
|
||||
}
|
||||
}
|
||||
|
||||
names = names.filter(name => ![
|
||||
'internalLink',
|
||||
'imageLink',
|
||||
'includeNoteLink',
|
||||
'relationMapLink'
|
||||
].includes(name));
|
||||
names = names.filter((name) => !["internalLink", "imageLink", "includeNoteLink", "relationMapLink"].includes(name));
|
||||
|
||||
names.sort((a, b) => {
|
||||
const aPrefix = a.toLowerCase().startsWith(nameLike);
|
||||
@@ -103,11 +100,7 @@ function isAttributeType(type: string): boolean {
|
||||
}
|
||||
|
||||
function isAttributeDangerous(type: string, name: string): boolean {
|
||||
return BUILTIN_ATTRIBUTES.some(attr =>
|
||||
attr.type === type &&
|
||||
attr.name.toLowerCase() === name.trim().toLowerCase() &&
|
||||
attr.isDangerous
|
||||
);
|
||||
return BUILTIN_ATTRIBUTES.some((attr) => attr.type === type && attr.name.toLowerCase() === name.trim().toLowerCase() && attr.isDangerous);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -7,18 +7,16 @@ import { isElectron } from "./utils.js";
|
||||
import passwordEncryptionService from "./encryption/password_encryption.js";
|
||||
import config from "./config.js";
|
||||
import passwordService from "./encryption/password.js";
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
const noAuthentication = config.General && config.General.noAuthentication === true;
|
||||
|
||||
function checkAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
res.redirect("setup");
|
||||
}
|
||||
else if (!req.session.loggedIn && !isElectron() && !noAuthentication) {
|
||||
} else if (!req.session.loggedIn && !isElectron() && !noAuthentication) {
|
||||
res.redirect("login");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -28,8 +26,7 @@ function checkAuth(req: Request, res: Response, next: NextFunction) {
|
||||
function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.loggedIn && !isElectron() && !noAuthentication) {
|
||||
reject(req, res, "Logged in session not found");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -37,8 +34,7 @@ function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction)
|
||||
function checkApiAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.loggedIn && !noAuthentication) {
|
||||
reject(req, res, "Logged in session not found");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -46,8 +42,7 @@ function checkApiAuth(req: Request, res: Response, next: NextFunction) {
|
||||
function checkAppInitialized(req: Request, res: Response, next: NextFunction) {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
res.redirect("setup");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -71,8 +66,7 @@ function checkPasswordNotSet(req: Request, res: Response, next: NextFunction) {
|
||||
function checkAppNotInitialized(req: Request, res: Response, next: NextFunction) {
|
||||
if (sqlInit.isDbInitialized()) {
|
||||
reject(req, res, "App already initialized.");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -80,8 +74,7 @@ function checkAppNotInitialized(req: Request, res: Response, next: NextFunction)
|
||||
function checkEtapiToken(req: Request, res: Response, next: NextFunction) {
|
||||
if (etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
|
||||
next();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
reject(req, res, "Token not found");
|
||||
}
|
||||
}
|
||||
@@ -89,45 +82,34 @@ function checkEtapiToken(req: Request, res: Response, next: NextFunction) {
|
||||
function reject(req: Request, res: Response, message: string) {
|
||||
log.info(`${req.method} ${req.path} rejected with 401 ${message}`);
|
||||
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
.status(401)
|
||||
.send(message);
|
||||
res.setHeader("Content-Type", "text/plain").status(401).send(message);
|
||||
}
|
||||
|
||||
function checkCredentials(req: Request, res: Response, next: NextFunction) {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
.status(400)
|
||||
.send('Database is not initialized yet.');
|
||||
res.setHeader("Content-Type", "text/plain").status(400).send("Database is not initialized yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordService.isPasswordSet()) {
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
.status(400)
|
||||
.send('Password has not been set yet. Please set a password and repeat the action');
|
||||
res.setHeader("Content-Type", "text/plain").status(400).send("Password has not been set yet. Please set a password and repeat the action");
|
||||
return;
|
||||
}
|
||||
|
||||
const header = req.headers['trilium-cred'] || '';
|
||||
const header = req.headers["trilium-cred"] || "";
|
||||
if (typeof header !== "string") {
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
.status(400)
|
||||
.send('Invalid data type for trilium-cred.');
|
||||
res.setHeader("Content-Type", "text/plain").status(400).send("Invalid data type for trilium-cred.");
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = Buffer.from(header, 'base64').toString();
|
||||
const colonIndex = auth.indexOf(':');
|
||||
const auth = Buffer.from(header, "base64").toString();
|
||||
const colonIndex = auth.indexOf(":");
|
||||
const password = colonIndex === -1 ? "" : auth.substr(colonIndex + 1);
|
||||
// username is ignored
|
||||
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
.status(401)
|
||||
.send('Incorrect password');
|
||||
}
|
||||
else {
|
||||
res.setHeader("Content-Type", "text/plain").status(401).send("Incorrect password");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import config from "./config.js";
|
||||
import axios from "axios";
|
||||
import dayjs from "dayjs";
|
||||
import xml2js from "xml2js";
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as cheerio from "cheerio";
|
||||
import cloningService from "./cloning.js";
|
||||
import appInfo from "./app_info.js";
|
||||
import searchService from "./search/services/search.js";
|
||||
@@ -31,11 +31,10 @@ import BAttachment from "../becca/entities/battachment.js";
|
||||
import BRevision from "../becca/entities/brevision.js";
|
||||
import BEtapiToken from "../becca/entities/betapi_token.js";
|
||||
import BOption from "../becca/entities/boption.js";
|
||||
import { AttributeRow } from '../becca/entities/rows.js';
|
||||
import Becca from '../becca/becca-interface.js';
|
||||
import { NoteParams } from './note-interface.js';
|
||||
import { ApiParams } from './backend_script_api_interface.js';
|
||||
|
||||
import { AttributeRow } from "../becca/entities/rows.js";
|
||||
import Becca from "../becca/becca-interface.js";
|
||||
import { NoteParams } from "./note-interface.js";
|
||||
import { ApiParams } from "./backend_script_api_interface.js";
|
||||
|
||||
/**
|
||||
* A whole number
|
||||
@@ -61,51 +60,51 @@ interface NoteAndBranch {
|
||||
|
||||
interface Api {
|
||||
/**
|
||||
* Note where the script started executing (entrypoint).
|
||||
* As an analogy, in C this would be the file which contains the main() function of the current process.
|
||||
*/
|
||||
* Note where the script started executing (entrypoint).
|
||||
* As an analogy, in C this would be the file which contains the main() function of the current process.
|
||||
*/
|
||||
startNote?: BNote | null;
|
||||
|
||||
/**
|
||||
* Note where the script is currently executing. This comes into play when your script is spread in multiple code
|
||||
* notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote).
|
||||
* A similar concept in C would be __FILE__
|
||||
* Don't mix this up with the concept of active note.
|
||||
*/
|
||||
* Note where the script is currently executing. This comes into play when your script is spread in multiple code
|
||||
* notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote).
|
||||
* A similar concept in C would be __FILE__
|
||||
* Don't mix this up with the concept of active note.
|
||||
*/
|
||||
currentNote: BNote;
|
||||
|
||||
/**
|
||||
* Entity whose event triggered this execution
|
||||
*/
|
||||
* Entity whose event triggered this execution
|
||||
*/
|
||||
originEntity?: AbstractBeccaEntity<any> | null;
|
||||
|
||||
/**
|
||||
* Axios library for HTTP requests. See {@link https://axios-http.com} for documentation
|
||||
* @deprecated use native (browser compatible) fetch() instead
|
||||
*/
|
||||
* Axios library for HTTP requests. See {@link https://axios-http.com} for documentation
|
||||
* @deprecated use native (browser compatible) fetch() instead
|
||||
*/
|
||||
axios: typeof axios;
|
||||
|
||||
/**
|
||||
* day.js library for date manipulation. See {@link https://day.js.org} for documentation
|
||||
*/
|
||||
* day.js library for date manipulation. See {@link https://day.js.org} for documentation
|
||||
*/
|
||||
dayjs: typeof dayjs;
|
||||
|
||||
/**
|
||||
* xml2js library for XML parsing. See {@link https://github.com/Leonidas-from-XIV/node-xml2js} for documentation
|
||||
*/
|
||||
* xml2js library for XML parsing. See {@link https://github.com/Leonidas-from-XIV/node-xml2js} for documentation
|
||||
*/
|
||||
|
||||
xml2js: typeof xml2js;
|
||||
|
||||
/**
|
||||
* cheerio library for HTML parsing and manipulation. See {@link https://cheerio.js.org} for documentation
|
||||
*/
|
||||
* cheerio library for HTML parsing and manipulation. See {@link https://cheerio.js.org} for documentation
|
||||
*/
|
||||
|
||||
cheerio: typeof cheerio;
|
||||
|
||||
/**
|
||||
* Instance name identifies particular Trilium instance. It can be useful for scripts
|
||||
* if some action needs to happen on only one specific instance.
|
||||
*/
|
||||
* Instance name identifies particular Trilium instance. It can be useful for scripts
|
||||
* if some action needs to happen on only one specific instance.
|
||||
*/
|
||||
getInstanceName(): string | null;
|
||||
|
||||
getNote(noteId: string): BNote | null;
|
||||
@@ -120,208 +119,224 @@ interface Api {
|
||||
getAttribute(attributeId: string): BAttribute | null;
|
||||
|
||||
/**
|
||||
* This is a powerful search method - you can search by attributes and their values, e.g.:
|
||||
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
|
||||
*/
|
||||
* This is a powerful search method - you can search by attributes and their values, e.g.:
|
||||
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
|
||||
*/
|
||||
searchForNotes(query: string, searchParams: SearchParams): BNote[];
|
||||
|
||||
/**
|
||||
* This is a powerful search method - you can search by attributes and their values, e.g.:
|
||||
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
|
||||
*/
|
||||
* This is a powerful search method - you can search by attributes and their values, e.g.:
|
||||
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
|
||||
*/
|
||||
searchForNote(query: string, searchParams: SearchParams): BNote | null;
|
||||
|
||||
/**
|
||||
* Retrieves notes with given label name & value
|
||||
*
|
||||
* @param name - attribute name
|
||||
* @param value - attribute value
|
||||
*/
|
||||
* Retrieves notes with given label name & value
|
||||
*
|
||||
* @param name - attribute name
|
||||
* @param value - attribute value
|
||||
*/
|
||||
getNotesWithLabel(name: string, value?: string): BNote[];
|
||||
|
||||
/**
|
||||
* Retrieves first note with given label name & value
|
||||
*
|
||||
* @param name - attribute name
|
||||
* @param value - attribute value
|
||||
*/
|
||||
* Retrieves first note with given label name & value
|
||||
*
|
||||
* @param name - attribute name
|
||||
* @param value - attribute value
|
||||
*/
|
||||
getNoteWithLabel(name: string, value?: string): BNote | null;
|
||||
|
||||
/**
|
||||
* If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch.
|
||||
*
|
||||
* @param prefix - if branch is created between note and parent note, set this prefix
|
||||
*/
|
||||
ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefix: string): {
|
||||
branch: BBranch | null
|
||||
* If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch.
|
||||
*
|
||||
* @param prefix - if branch is created between note and parent note, set this prefix
|
||||
*/
|
||||
ensureNoteIsPresentInParent(
|
||||
noteId: string,
|
||||
parentNoteId: string,
|
||||
prefix: string
|
||||
): {
|
||||
branch: BBranch | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* If there's a branch between note and parent note, remove it. Otherwise, do nothing.
|
||||
*/
|
||||
* If there's a branch between note and parent note, remove it. Otherwise, do nothing.
|
||||
*/
|
||||
ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string): void;
|
||||
|
||||
/**
|
||||
* Based on the value, either create or remove branch between note and parent note.
|
||||
*
|
||||
* @param present - true if we want the branch to exist, false if we want it gone
|
||||
* @param prefix - if branch is created between note and parent note, set this prefix
|
||||
*/
|
||||
* Based on the value, either create or remove branch between note and parent note.
|
||||
*
|
||||
* @param present - true if we want the branch to exist, false if we want it gone
|
||||
* @param prefix - if branch is created between note and parent note, set this prefix
|
||||
*/
|
||||
toggleNoteInParent(present: true, noteId: string, parentNoteId: string, prefix: string): void;
|
||||
|
||||
/**
|
||||
* Create text note. See also createNewNote() for more options.
|
||||
*/
|
||||
* Create text note. See also createNewNote() for more options.
|
||||
*/
|
||||
createTextNote(parentNoteId: string, title: string, content: string): NoteAndBranch;
|
||||
|
||||
/**
|
||||
* Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
|
||||
* JSON MIME type. See also createNewNote() for more options.
|
||||
*/
|
||||
* Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
|
||||
* JSON MIME type. See also createNewNote() for more options.
|
||||
*/
|
||||
createDataNote(parentNoteId: string, title: string, content: {}): NoteAndBranch;
|
||||
|
||||
/**
|
||||
* @returns object contains newly created entities note and branch
|
||||
*/
|
||||
* @returns object contains newly created entities note and branch
|
||||
*/
|
||||
createNewNote(params: NoteParams): NoteAndBranch;
|
||||
|
||||
/**
|
||||
* @deprecated please use createTextNote() with similar API for simpler use cases or createNewNote() for more complex needs
|
||||
* @param parentNoteId - create new note under this parent
|
||||
* @returns object contains newly created entities note and branch
|
||||
*/
|
||||
createNote(parentNoteId: string, title: string, content: string, extraOptions: Omit<NoteParams, "title" | "content" | "type" | "parentNoteId"> & {
|
||||
/** should the note be JSON */
|
||||
json?: boolean;
|
||||
attributes?: AttributeRow[]
|
||||
}): NoteAndBranch;
|
||||
* @deprecated please use createTextNote() with similar API for simpler use cases or createNewNote() for more complex needs
|
||||
* @param parentNoteId - create new note under this parent
|
||||
* @returns object contains newly created entities note and branch
|
||||
*/
|
||||
createNote(
|
||||
parentNoteId: string,
|
||||
title: string,
|
||||
content: string,
|
||||
extraOptions: Omit<NoteParams, "title" | "content" | "type" | "parentNoteId"> & {
|
||||
/** should the note be JSON */
|
||||
json?: boolean;
|
||||
attributes?: AttributeRow[];
|
||||
}
|
||||
): NoteAndBranch;
|
||||
|
||||
logMessages: Record<string, string[]>;
|
||||
logSpacedUpdates: Record<string, SpacedUpdate>;
|
||||
|
||||
/**
|
||||
* Log given message to trilium logs and log pane in UI
|
||||
*/
|
||||
* Log given message to trilium logs and log pane in UI
|
||||
*/
|
||||
log(message: string): void;
|
||||
|
||||
/**
|
||||
* Returns root note of the calendar.
|
||||
*/
|
||||
* Returns root note of the calendar.
|
||||
*/
|
||||
getRootCalendarNote(): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns day note for given date. If such note doesn't exist, it is created.
|
||||
*
|
||||
* @method
|
||||
* @param date in YYYY-MM-DD format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
* Returns day note for given date. If such note doesn't exist, it is created.
|
||||
*
|
||||
* @method
|
||||
* @param date in YYYY-MM-DD format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getDayNote(date: string, rootNote?: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns today's day note. If such note doesn't exist, it is created.
|
||||
*
|
||||
* @param rootNote specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
* Returns today's day note. If such note doesn't exist, it is created.
|
||||
*
|
||||
* @param rootNote specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getTodayNote(rootNote?: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns note for the first date of the week of the given date.
|
||||
*
|
||||
* @param date in YYYY-MM-DD format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getWeekNote(date: string, options: {
|
||||
// TODO: Deduplicate type with date_notes.ts once ES modules are added.
|
||||
/** either "monday" (default) or "sunday" */
|
||||
startOfTheWeek: "monday" | "sunday";
|
||||
}, rootNote: BNote): BNote | null;
|
||||
* Returns note for the first date of the week of the given date.
|
||||
*
|
||||
* @param date in YYYY-MM-DD format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getWeekNote(
|
||||
date: string,
|
||||
options: {
|
||||
// TODO: Deduplicate type with date_notes.ts once ES modules are added.
|
||||
/** either "monday" (default) or "sunday" */
|
||||
startOfTheWeek: "monday" | "sunday";
|
||||
},
|
||||
rootNote: BNote
|
||||
): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns month note for given date. If such a note doesn't exist, it is created.
|
||||
*
|
||||
* @param date in YYYY-MM format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
* Returns month note for given date. If such a note doesn't exist, it is created.
|
||||
*
|
||||
* @param date in YYYY-MM format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getMonthNote(date: string, rootNote: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Returns year note for given year. If such a note doesn't exist, it is created.
|
||||
*
|
||||
* @param year in YYYY format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
* Returns year note for given year. If such a note doesn't exist, it is created.
|
||||
*
|
||||
* @param year in YYYY format
|
||||
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
|
||||
*/
|
||||
getYearNote(year: string, rootNote?: BNote): BNote | null;
|
||||
|
||||
/**
|
||||
* Sort child notes of a given note.
|
||||
*/
|
||||
sortNotes(parentNoteId: string, sortConfig: {
|
||||
/** 'title', 'dateCreated', 'dateModified' or a label name
|
||||
* See {@link https://triliumnext.github.io/Docs/Wiki/sorting.html} for details. */
|
||||
sortBy?: string;
|
||||
reverse?: boolean;
|
||||
foldersFirst?: boolean;
|
||||
}): void;
|
||||
* Sort child notes of a given note.
|
||||
*/
|
||||
sortNotes(
|
||||
parentNoteId: string,
|
||||
sortConfig: {
|
||||
/** 'title', 'dateCreated', 'dateModified' or a label name
|
||||
* See {@link https://triliumnext.github.io/Docs/Wiki/sorting.html} for details. */
|
||||
sortBy?: string;
|
||||
reverse?: boolean;
|
||||
foldersFirst?: boolean;
|
||||
}
|
||||
): void;
|
||||
|
||||
/**
|
||||
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
|
||||
* or removes the branch (if parentNoteId is not given).
|
||||
*
|
||||
* This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix.
|
||||
*
|
||||
* @deprecated this method is pretty confusing and serves specialized purpose only
|
||||
*/
|
||||
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
|
||||
* or removes the branch (if parentNoteId is not given).
|
||||
*
|
||||
* This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix.
|
||||
*
|
||||
* @deprecated this method is pretty confusing and serves specialized purpose only
|
||||
*/
|
||||
setNoteToParent(noteId: string, prefix: string, parentNoteId: string | null): void;
|
||||
|
||||
/**
|
||||
* This functions wraps code which is supposed to be running in transaction. If transaction already
|
||||
* exists, then we'll use that transaction.
|
||||
*
|
||||
* @param func
|
||||
* @returns result of func callback
|
||||
*/
|
||||
* This functions wraps code which is supposed to be running in transaction. If transaction already
|
||||
* exists, then we'll use that transaction.
|
||||
*
|
||||
* @param func
|
||||
* @returns result of func callback
|
||||
*/
|
||||
transactional(func: () => void): any;
|
||||
|
||||
/**
|
||||
* Return randomly generated string of given length. This random string generation is NOT cryptographically secure.
|
||||
*
|
||||
* @param length of the string
|
||||
* @returns random string
|
||||
*/
|
||||
* Return randomly generated string of given length. This random string generation is NOT cryptographically secure.
|
||||
*
|
||||
* @param length of the string
|
||||
* @returns random string
|
||||
*/
|
||||
randomString(length: number): string;
|
||||
|
||||
/**
|
||||
* @param to escape
|
||||
* @returns escaped string
|
||||
*/
|
||||
* @param to escape
|
||||
* @returns escaped string
|
||||
*/
|
||||
escapeHtml(string: string): string;
|
||||
|
||||
/**
|
||||
* @param string to unescape
|
||||
* @returns unescaped string
|
||||
*/
|
||||
* @param string to unescape
|
||||
* @returns unescaped string
|
||||
*/
|
||||
unescapeHtml(string: string): string;
|
||||
|
||||
/**
|
||||
* sql
|
||||
* @type {module:sql}
|
||||
*/
|
||||
* sql
|
||||
* @type {module:sql}
|
||||
*/
|
||||
sql: any;
|
||||
|
||||
getAppInfo(): typeof appInfo;
|
||||
|
||||
/**
|
||||
* Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated.
|
||||
*/
|
||||
* Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated.
|
||||
*/
|
||||
createOrUpdateLauncher(opts: {
|
||||
/** id of the launcher, only alphanumeric at least 6 characters long */
|
||||
id: string;
|
||||
/** one of
|
||||
* - "note" - activating the launcher will navigate to the target note (specified in targetNoteId param)
|
||||
* - "script" - activating the launcher will execute the script (specified in scriptNoteId param)
|
||||
* - "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param)
|
||||
*/
|
||||
* - "note" - activating the launcher will navigate to the target note (specified in targetNoteId param)
|
||||
* - "script" - activating the launcher will execute the script (specified in scriptNoteId param)
|
||||
* - "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param)
|
||||
*/
|
||||
type: "note" | "script" | "customWidget";
|
||||
title: string;
|
||||
/** if true, will be created in the "Visible launchers", otherwise in "Available launchers" */
|
||||
@@ -339,44 +354,44 @@ interface Api {
|
||||
}): { note: BNote };
|
||||
|
||||
/**
|
||||
* @param format - either 'html' or 'markdown'
|
||||
*/
|
||||
* @param format - either 'html' or 'markdown'
|
||||
*/
|
||||
exportSubtreeToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Executes given anonymous function on the frontend(s).
|
||||
* Internally, this serializes the anonymous function into string and sends it to frontend(s) via WebSocket.
|
||||
* Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all
|
||||
* instances execute the given function.
|
||||
*
|
||||
* @param script - script to be executed on the frontend
|
||||
* @param params - list of parameters to the anonymous function to be sent to frontend
|
||||
* @returns no return value is provided.
|
||||
*/
|
||||
* Executes given anonymous function on the frontend(s).
|
||||
* Internally, this serializes the anonymous function into string and sends it to frontend(s) via WebSocket.
|
||||
* Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all
|
||||
* instances execute the given function.
|
||||
*
|
||||
* @param script - script to be executed on the frontend
|
||||
* @param params - list of parameters to the anonymous function to be sent to frontend
|
||||
* @returns no return value is provided.
|
||||
*/
|
||||
runOnFrontend(script: () => void | string, params: []): void;
|
||||
|
||||
/**
|
||||
* Sync process can make data intermittently inconsistent. Scripts which require strong data consistency
|
||||
* can use this function to wait for a possible sync process to finish and prevent new sync process from starting
|
||||
* while it is running.
|
||||
*
|
||||
* Because this is an async process, the inner callback doesn't have automatic transaction handling, so in case
|
||||
* you need to make some DB changes, you need to surround your call with api.transactional(...)
|
||||
*
|
||||
* @param callback - function to be executed while sync process is not running
|
||||
* @returns resolves once the callback is finished (callback is awaited)
|
||||
*/
|
||||
* Sync process can make data intermittently inconsistent. Scripts which require strong data consistency
|
||||
* can use this function to wait for a possible sync process to finish and prevent new sync process from starting
|
||||
* while it is running.
|
||||
*
|
||||
* Because this is an async process, the inner callback doesn't have automatic transaction handling, so in case
|
||||
* you need to make some DB changes, you need to surround your call with api.transactional(...)
|
||||
*
|
||||
* @param callback - function to be executed while sync process is not running
|
||||
* @returns resolves once the callback is finished (callback is awaited)
|
||||
*/
|
||||
runOutsideOfSync(callback: () => void): Promise<void>;
|
||||
|
||||
/**
|
||||
* @param backupName - If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file
|
||||
* @returns resolves once the backup is finished
|
||||
*/
|
||||
* @param backupName - If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file
|
||||
* @returns resolves once the backup is finished
|
||||
*/
|
||||
backupNow(backupName: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* This object contains "at your risk" and "no BC guarantees" objects for advanced use cases.
|
||||
*/
|
||||
* This object contains "at your risk" and "no BC guarantees" objects for advanced use cases.
|
||||
*/
|
||||
__private: {
|
||||
/** provides access to the backend in-memory object graph, see {@link Becca} */
|
||||
becca: Becca;
|
||||
@@ -405,17 +420,17 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
this.dayjs = dayjs;
|
||||
this.xml2js = xml2js;
|
||||
this.cheerio = cheerio;
|
||||
this.getInstanceName = () => config.General ? config.General.instanceName : null;
|
||||
this.getNote = noteId => becca.getNote(noteId);
|
||||
this.getBranch = branchId => becca.getBranch(branchId);
|
||||
this.getAttribute = attributeId => becca.getAttribute(attributeId);
|
||||
this.getAttachment = attachmentId => becca.getAttachment(attachmentId);
|
||||
this.getRevision = revisionId => becca.getRevision(revisionId);
|
||||
this.getEtapiToken = etapiTokenId => becca.getEtapiToken(etapiTokenId);
|
||||
this.getInstanceName = () => (config.General ? config.General.instanceName : null);
|
||||
this.getNote = (noteId) => becca.getNote(noteId);
|
||||
this.getBranch = (branchId) => becca.getBranch(branchId);
|
||||
this.getAttribute = (attributeId) => becca.getAttribute(attributeId);
|
||||
this.getAttachment = (attachmentId) => becca.getAttachment(attachmentId);
|
||||
this.getRevision = (revisionId) => becca.getRevision(revisionId);
|
||||
this.getEtapiToken = (etapiTokenId) => becca.getEtapiToken(etapiTokenId);
|
||||
this.getEtapiTokens = () => becca.getEtapiTokens();
|
||||
this.getOption = optionName => becca.getOption(optionName);
|
||||
this.getOption = (optionName) => becca.getOption(optionName);
|
||||
this.getOptions = () => optionsService.getOptions();
|
||||
this.getAttribute = attributeId => becca.getAttribute(attributeId);
|
||||
this.getAttribute = (attributeId) => becca.getAttribute(attributeId);
|
||||
|
||||
this.searchForNotes = (query, searchParams = {}) => {
|
||||
if (searchParams.includeArchivedNotes === undefined) {
|
||||
@@ -426,13 +441,11 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
searchParams.ignoreHoistedNote = true;
|
||||
}
|
||||
|
||||
const noteIds = searchService.findResultsWithQuery(query, new SearchContext(searchParams))
|
||||
.map(sr => sr.noteId);
|
||||
const noteIds = searchService.findResultsWithQuery(query, new SearchContext(searchParams)).map((sr) => sr.noteId);
|
||||
|
||||
return becca.getNotes(noteIds);
|
||||
};
|
||||
|
||||
|
||||
this.searchForNote = (query, searchParams = {}) => {
|
||||
const notes = this.searchForNotes(query, searchParams);
|
||||
|
||||
@@ -444,20 +457,22 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
|
||||
this.ensureNoteIsAbsentFromParent = cloningService.ensureNoteIsAbsentFromParent;
|
||||
this.toggleNoteInParent = cloningService.toggleNoteInParent;
|
||||
this.createTextNote = (parentNoteId, title, content = '') => noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title,
|
||||
content,
|
||||
type: 'text'
|
||||
});
|
||||
this.createTextNote = (parentNoteId, title, content = "") =>
|
||||
noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title,
|
||||
content,
|
||||
type: "text"
|
||||
});
|
||||
|
||||
this.createDataNote = (parentNoteId, title, content = {}) => noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title,
|
||||
content: JSON.stringify(content, null, '\t'),
|
||||
type: 'code',
|
||||
mime: 'application/json'
|
||||
});
|
||||
this.createDataNote = (parentNoteId, title, content = {}) =>
|
||||
noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title,
|
||||
content: JSON.stringify(content, null, "\t"),
|
||||
type: "code",
|
||||
mime: "application/json"
|
||||
});
|
||||
|
||||
this.createNewNote = noteService.createNewNote;
|
||||
|
||||
@@ -476,15 +491,14 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
};
|
||||
|
||||
// code note type can be inherited, otherwise "text" is the default
|
||||
extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
|
||||
extraOptions.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
|
||||
extraOptions.type = parentNote.type === "code" ? "code" : "text";
|
||||
extraOptions.mime = parentNote.type === "code" ? parentNote.mime : "text/html";
|
||||
|
||||
if (_extraOptions.json) {
|
||||
extraOptions.content = JSON.stringify(content || {}, null, '\t');
|
||||
extraOptions.type = 'code';
|
||||
extraOptions.mime = 'application/json';
|
||||
}
|
||||
else {
|
||||
extraOptions.content = JSON.stringify(content || {}, null, "\t");
|
||||
extraOptions.type = "code";
|
||||
extraOptions.mime = "application/json";
|
||||
} else {
|
||||
extraOptions.content = content;
|
||||
}
|
||||
|
||||
@@ -508,7 +522,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
this.logMessages = {};
|
||||
this.logSpacedUpdates = {};
|
||||
|
||||
this.log = message => {
|
||||
this.log = (message) => {
|
||||
log.info(message);
|
||||
|
||||
if (!this.startNote) {
|
||||
@@ -518,16 +532,18 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
const { noteId } = this.startNote;
|
||||
|
||||
this.logMessages[noteId] = this.logMessages[noteId] || [];
|
||||
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
|
||||
const messages = this.logMessages[noteId];
|
||||
this.logMessages[noteId] = [];
|
||||
this.logSpacedUpdates[noteId] =
|
||||
this.logSpacedUpdates[noteId] ||
|
||||
new SpacedUpdate(() => {
|
||||
const messages = this.logMessages[noteId];
|
||||
this.logMessages[noteId] = [];
|
||||
|
||||
ws.sendMessageToAllClients({
|
||||
type: 'api-log-messages',
|
||||
noteId,
|
||||
messages
|
||||
});
|
||||
}, 100);
|
||||
ws.sendMessageToAllClients({
|
||||
type: "api-log-messages",
|
||||
noteId,
|
||||
messages
|
||||
});
|
||||
}, 100);
|
||||
|
||||
this.logMessages[noteId].push(message);
|
||||
this.logSpacedUpdates[noteId].scheduleUpdate();
|
||||
@@ -540,12 +556,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
this.getMonthNote = dateNoteService.getMonthNote;
|
||||
this.getYearNote = dateNoteService.getYearNote;
|
||||
|
||||
this.sortNotes = (parentNoteId, sortConfig = {}) => treeService.sortNotes(
|
||||
parentNoteId,
|
||||
sortConfig.sortBy || "title",
|
||||
!!sortConfig.reverse,
|
||||
!!sortConfig.foldersFirst
|
||||
);
|
||||
this.sortNotes = (parentNoteId, sortConfig = {}) => treeService.sortNotes(parentNoteId, sortConfig.sortBy || "title", !!sortConfig.reverse, !!sortConfig.foldersFirst);
|
||||
|
||||
this.setNoteToParent = treeService.setNoteToParent;
|
||||
this.transactional = sql.transactional;
|
||||
@@ -555,26 +566,41 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
this.sql = sql;
|
||||
this.getAppInfo = () => appInfo;
|
||||
|
||||
this.createOrUpdateLauncher = (opts) => {
|
||||
if (!opts.id) {
|
||||
throw new Error("ID is a mandatory parameter for api.createOrUpdateLauncher(opts)");
|
||||
}
|
||||
if (!opts.id.match(/[a-z0-9]{6,1000}/i)) {
|
||||
throw new Error(`ID must be an alphanumeric string at least 6 characters long.`);
|
||||
}
|
||||
if (!opts.type) {
|
||||
throw new Error("Launcher Type is a mandatory parameter for api.createOrUpdateLauncher(opts)");
|
||||
}
|
||||
if (!["note", "script", "customWidget"].includes(opts.type)) {
|
||||
throw new Error(`Given launcher type '${opts.type}'`);
|
||||
}
|
||||
if (!opts.title?.trim()) {
|
||||
throw new Error("Title is a mandatory parameter for api.createOrUpdateLauncher(opts)");
|
||||
}
|
||||
if (opts.type === "note" && !opts.targetNoteId) {
|
||||
throw new Error("targetNoteId is mandatory for launchers of type 'note'");
|
||||
}
|
||||
if (opts.type === "script" && !opts.scriptNoteId) {
|
||||
throw new Error("scriptNoteId is mandatory for launchers of type 'script'");
|
||||
}
|
||||
if (opts.type === "customWidget" && !opts.widgetNoteId) {
|
||||
throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'");
|
||||
}
|
||||
|
||||
this.createOrUpdateLauncher = opts => {
|
||||
if (!opts.id) { throw new Error("ID is a mandatory parameter for api.createOrUpdateLauncher(opts)"); }
|
||||
if (!opts.id.match(/[a-z0-9]{6,1000}/i)) { throw new Error(`ID must be an alphanumeric string at least 6 characters long.`); }
|
||||
if (!opts.type) { throw new Error("Launcher Type is a mandatory parameter for api.createOrUpdateLauncher(opts)"); }
|
||||
if (!["note", "script", "customWidget"].includes(opts.type)) { throw new Error(`Given launcher type '${opts.type}'`); }
|
||||
if (!opts.title?.trim()) { throw new Error("Title is a mandatory parameter for api.createOrUpdateLauncher(opts)"); }
|
||||
if (opts.type === 'note' && !opts.targetNoteId) { throw new Error("targetNoteId is mandatory for launchers of type 'note'"); }
|
||||
if (opts.type === 'script' && !opts.scriptNoteId) { throw new Error("scriptNoteId is mandatory for launchers of type 'script'"); }
|
||||
if (opts.type === 'customWidget' && !opts.widgetNoteId) { throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'"); }
|
||||
|
||||
const parentNoteId = opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers';
|
||||
const noteId = 'al_' + opts.id;
|
||||
const parentNoteId = opts.isVisible ? "_lbVisibleLaunchers" : "_lbAvailableLaunchers";
|
||||
const noteId = "al_" + opts.id;
|
||||
|
||||
const launcherNote =
|
||||
becca.getNote(noteId) ||
|
||||
specialNotesService.createLauncher({
|
||||
noteId: noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
launcherType: opts.type,
|
||||
launcherType: opts.type
|
||||
}).note;
|
||||
|
||||
if (launcherNote.title !== opts.title) {
|
||||
@@ -590,26 +616,26 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.type === 'note') {
|
||||
launcherNote.setRelation('target', opts.targetNoteId);
|
||||
} else if (opts.type === 'script') {
|
||||
launcherNote.setRelation('script', opts.scriptNoteId);
|
||||
} else if (opts.type === 'customWidget') {
|
||||
launcherNote.setRelation('widget', opts.widgetNoteId);
|
||||
if (opts.type === "note") {
|
||||
launcherNote.setRelation("target", opts.targetNoteId);
|
||||
} else if (opts.type === "script") {
|
||||
launcherNote.setRelation("script", opts.scriptNoteId);
|
||||
} else if (opts.type === "customWidget") {
|
||||
launcherNote.setRelation("widget", opts.widgetNoteId);
|
||||
} else {
|
||||
throw new Error(`Unrecognized launcher type '${opts.type}'`);
|
||||
}
|
||||
|
||||
if (opts.keyboardShortcut) {
|
||||
launcherNote.setLabel('keyboardShortcut', opts.keyboardShortcut);
|
||||
launcherNote.setLabel("keyboardShortcut", opts.keyboardShortcut);
|
||||
} else {
|
||||
launcherNote.removeLabel('keyboardShortcut');
|
||||
launcherNote.removeLabel("keyboardShortcut");
|
||||
}
|
||||
|
||||
if (opts.icon) {
|
||||
launcherNote.setLabel('iconClass', `bx ${opts.icon}`);
|
||||
launcherNote.setLabel("iconClass", `bx ${opts.icon}`);
|
||||
} else {
|
||||
launcherNote.removeLabel('iconClass');
|
||||
launcherNote.removeLabel("iconClass");
|
||||
}
|
||||
|
||||
return { note: launcherNote };
|
||||
@@ -626,7 +652,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
}
|
||||
|
||||
ws.sendMessageToAllClients({
|
||||
type: 'execute-script',
|
||||
type: "execute-script",
|
||||
script: script,
|
||||
params: prepareParams(params),
|
||||
startNoteId: this.startNote?.noteId,
|
||||
@@ -640,11 +666,10 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return params.map(p => {
|
||||
return params.map((p) => {
|
||||
if (typeof p === "function") {
|
||||
return `!@#Function: ${p.toString()}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return p;
|
||||
}
|
||||
});
|
||||
@@ -656,9 +681,9 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
|
||||
|
||||
this.__private = {
|
||||
becca
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default BackendScriptApi as any as {
|
||||
new (currentNote: BNote, apiParams: ApiParams): Api
|
||||
new (currentNote: BNote, apiParams: ApiParams): Api;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import BNote from "../becca/entities/bnote.js";
|
||||
export interface ApiParams {
|
||||
startNote?: BNote | null;
|
||||
originEntity?: AbstractBeccaEntity<any> | null;
|
||||
pathParams?: string[],
|
||||
req?: Request,
|
||||
res?: Response
|
||||
pathParams?: string[];
|
||||
req?: Request;
|
||||
res?: Response;
|
||||
}
|
||||
|
||||
@@ -11,30 +11,31 @@ import sql from "./sql.js";
|
||||
import path from "path";
|
||||
import type { OptionNames } from "./options_interface.js";
|
||||
|
||||
type BackupType = ("daily" | "weekly" | "monthly");
|
||||
type BackupType = "daily" | "weekly" | "monthly";
|
||||
|
||||
function getExistingBackups() {
|
||||
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs.readdirSync(dataDir.BACKUP_DIR)
|
||||
.filter(fileName => fileName.includes("backup"))
|
||||
.map(fileName => {
|
||||
return fs
|
||||
.readdirSync(dataDir.BACKUP_DIR)
|
||||
.filter((fileName) => fileName.includes("backup"))
|
||||
.map((fileName) => {
|
||||
const filePath = path.resolve(dataDir.BACKUP_DIR, fileName);
|
||||
const stat = fs.statSync(filePath)
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
return {fileName, filePath, mtime: stat.mtime};
|
||||
return { fileName, filePath, mtime: stat.mtime };
|
||||
});
|
||||
}
|
||||
|
||||
function regularBackup() {
|
||||
cls.init(() => {
|
||||
periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
|
||||
periodBackup("lastDailyBackupDate", "daily", 24 * 3600);
|
||||
|
||||
periodBackup('lastWeeklyBackupDate', 'weekly', 7 * 24 * 3600);
|
||||
periodBackup("lastWeeklyBackupDate", "weekly", 7 * 24 * 3600);
|
||||
|
||||
periodBackup('lastMonthlyBackupDate', 'monthly', 30 * 24 * 3600);
|
||||
periodBackup("lastMonthlyBackupDate", "monthly", 30 * 24 * 3600);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ function processContent(content: Buffer | string | null, isProtected: boolean, i
|
||||
}
|
||||
}
|
||||
|
||||
function calculateContentHash({blobId, content}: Blob) {
|
||||
function calculateContentHash({ blobId, content }: Blob) {
|
||||
return hash(`${blobId}|${content.toString()}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import BBranch from "../becca/entities/bbranch.js";
|
||||
|
||||
function moveBranchToNote(branchToMove: BBranch, targetParentNoteId: string) {
|
||||
if (branchToMove.parentNoteId === targetParentNoteId) {
|
||||
return {success: true}; // no-op
|
||||
return { success: true }; // no-op
|
||||
}
|
||||
|
||||
const validationResult = treeService.validateParentChild(targetParentNoteId, branchToMove.noteId, branchToMove.branchId);
|
||||
@@ -13,7 +13,7 @@ function moveBranchToNote(branchToMove: BBranch, targetParentNoteId: string) {
|
||||
return [200, validationResult];
|
||||
}
|
||||
|
||||
const maxNotePos = sql.getValue<number | null>('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [targetParentNoteId]);
|
||||
const maxNotePos = sql.getValue<number | null>("SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [targetParentNoteId]);
|
||||
const newNotePos = !maxNotePos ? 0 : maxNotePos + 10;
|
||||
|
||||
const newBranch = branchToMove.createClone(targetParentNoteId, newNotePos);
|
||||
|
||||
@@ -1,99 +1,99 @@
|
||||
export default [
|
||||
// label names
|
||||
{ type: 'label', name: 'inbox' },
|
||||
{ type: 'label', name: 'disableVersioning' },
|
||||
{ type: 'label', name: 'calendarRoot' },
|
||||
{ type: 'label', name: 'archived' },
|
||||
{ type: 'label', name: 'excludeFromExport' },
|
||||
{ type: 'label', name: 'disableInclusion' },
|
||||
{ type: 'label', name: 'appCss' },
|
||||
{ type: 'label', name: 'appTheme' },
|
||||
{ type: 'label', name: 'appThemeBase' },
|
||||
{ type: 'label', name: 'hidePromotedAttributes' },
|
||||
{ type: 'label', name: 'readOnly' },
|
||||
{ type: 'label', name: 'autoReadOnlyDisabled' },
|
||||
{ type: 'label', name: 'cssClass' },
|
||||
{ type: 'label', name: 'iconClass' },
|
||||
{ type: 'label', name: 'keyboardShortcut' },
|
||||
{ type: 'label', name: 'run', isDangerous: true },
|
||||
{ type: 'label', name: 'runOnInstance', isDangerous: false },
|
||||
{ type: 'label', name: 'runAtHour', isDangerous: false },
|
||||
{ type: 'label', name: 'customRequestHandler', isDangerous: true },
|
||||
{ type: 'label', name: 'customResourceProvider', isDangerous: true },
|
||||
{ type: 'label', name: 'widget', isDangerous: true },
|
||||
{ type: 'label', name: 'noteInfoWidgetDisabled' },
|
||||
{ type: 'label', name: 'linkMapWidgetDisabled' },
|
||||
{ type: 'label', name: 'revisionsWidgetDisabled' },
|
||||
{ type: 'label', name: 'whatLinksHereWidgetDisabled' },
|
||||
{ type: 'label', name: 'similarNotesWidgetDisabled' },
|
||||
{ type: 'label', name: 'workspace' },
|
||||
{ type: 'label', name: 'workspaceIconClass' },
|
||||
{ type: 'label', name: 'workspaceTabBackgroundColor' },
|
||||
{ type: 'label', name: 'workspaceCalendarRoot' },
|
||||
{ type: 'label', name: 'workspaceTemplate' },
|
||||
{ type: 'label', name: 'searchHome' },
|
||||
{ type: 'label', name: 'workspaceInbox' },
|
||||
{ type: 'label', name: 'workspaceSearchHome' },
|
||||
{ type: 'label', name: 'sqlConsoleHome' },
|
||||
{ type: 'label', name: 'datePattern' },
|
||||
{ type: 'label', name: 'pageSize' },
|
||||
{ type: 'label', name: 'viewType' },
|
||||
{ type: 'label', name: 'mapRootNoteId' },
|
||||
{ type: 'label', name: 'bookmarkFolder' },
|
||||
{ type: 'label', name: 'sorted' },
|
||||
{ type: 'label', name: 'sortDirection' },
|
||||
{ type: 'label', name: 'sortFoldersFirst' },
|
||||
{ type: 'label', name: 'sortNatural' },
|
||||
{ type: 'label', name: 'sortLocale' },
|
||||
{ type: 'label', name: 'top' },
|
||||
{ type: 'label', name: 'bottom' },
|
||||
{ type: 'label', name: 'fullContentWidth' },
|
||||
{ type: 'label', name: 'shareHiddenFromTree' },
|
||||
{ type: 'label', name: 'shareExternalLink' },
|
||||
{ type: 'label', name: 'shareAlias' },
|
||||
{ type: 'label', name: 'shareOmitDefaultCss' },
|
||||
{ type: 'label', name: 'shareRoot' },
|
||||
{ type: 'label', name: 'shareDescription' },
|
||||
{ type: 'label', name: 'shareRaw' },
|
||||
{ type: 'label', name: 'shareDisallowRobotIndexing' },
|
||||
{ type: 'label', name: 'shareCredentials' },
|
||||
{ type: 'label', name: 'shareIndex' },
|
||||
{ type: 'label', name: 'displayRelations' },
|
||||
{ type: 'label', name: 'hideRelations' },
|
||||
{ type: 'label', name: 'titleTemplate', isDangerous: true },
|
||||
{ type: 'label', name: 'template' },
|
||||
{ type: 'label', name: 'toc' },
|
||||
{ type: 'label', name: 'color' },
|
||||
{ type: 'label', name: 'keepCurrentHoisting'},
|
||||
{ type: 'label', name: 'executeButton'},
|
||||
{ type: 'label', name: 'executeDescription'},
|
||||
{ type: 'label', name: 'newNotesOnTop'},
|
||||
{ type: 'label', name: 'clipperInbox'},
|
||||
{ type: 'label', name: 'webViewSrc', isDangerous: true },
|
||||
{ type: 'label', name: 'hideHighlightWidget' },
|
||||
{ type: "label", name: "inbox" },
|
||||
{ type: "label", name: "disableVersioning" },
|
||||
{ type: "label", name: "calendarRoot" },
|
||||
{ type: "label", name: "archived" },
|
||||
{ type: "label", name: "excludeFromExport" },
|
||||
{ type: "label", name: "disableInclusion" },
|
||||
{ type: "label", name: "appCss" },
|
||||
{ type: "label", name: "appTheme" },
|
||||
{ type: "label", name: "appThemeBase" },
|
||||
{ type: "label", name: "hidePromotedAttributes" },
|
||||
{ type: "label", name: "readOnly" },
|
||||
{ type: "label", name: "autoReadOnlyDisabled" },
|
||||
{ type: "label", name: "cssClass" },
|
||||
{ type: "label", name: "iconClass" },
|
||||
{ type: "label", name: "keyboardShortcut" },
|
||||
{ type: "label", name: "run", isDangerous: true },
|
||||
{ type: "label", name: "runOnInstance", isDangerous: false },
|
||||
{ type: "label", name: "runAtHour", isDangerous: false },
|
||||
{ type: "label", name: "customRequestHandler", isDangerous: true },
|
||||
{ type: "label", name: "customResourceProvider", isDangerous: true },
|
||||
{ type: "label", name: "widget", isDangerous: true },
|
||||
{ type: "label", name: "noteInfoWidgetDisabled" },
|
||||
{ type: "label", name: "linkMapWidgetDisabled" },
|
||||
{ type: "label", name: "revisionsWidgetDisabled" },
|
||||
{ type: "label", name: "whatLinksHereWidgetDisabled" },
|
||||
{ type: "label", name: "similarNotesWidgetDisabled" },
|
||||
{ type: "label", name: "workspace" },
|
||||
{ type: "label", name: "workspaceIconClass" },
|
||||
{ type: "label", name: "workspaceTabBackgroundColor" },
|
||||
{ type: "label", name: "workspaceCalendarRoot" },
|
||||
{ type: "label", name: "workspaceTemplate" },
|
||||
{ type: "label", name: "searchHome" },
|
||||
{ type: "label", name: "workspaceInbox" },
|
||||
{ type: "label", name: "workspaceSearchHome" },
|
||||
{ type: "label", name: "sqlConsoleHome" },
|
||||
{ type: "label", name: "datePattern" },
|
||||
{ type: "label", name: "pageSize" },
|
||||
{ type: "label", name: "viewType" },
|
||||
{ type: "label", name: "mapRootNoteId" },
|
||||
{ type: "label", name: "bookmarkFolder" },
|
||||
{ type: "label", name: "sorted" },
|
||||
{ type: "label", name: "sortDirection" },
|
||||
{ type: "label", name: "sortFoldersFirst" },
|
||||
{ type: "label", name: "sortNatural" },
|
||||
{ type: "label", name: "sortLocale" },
|
||||
{ type: "label", name: "top" },
|
||||
{ type: "label", name: "bottom" },
|
||||
{ type: "label", name: "fullContentWidth" },
|
||||
{ type: "label", name: "shareHiddenFromTree" },
|
||||
{ type: "label", name: "shareExternalLink" },
|
||||
{ type: "label", name: "shareAlias" },
|
||||
{ type: "label", name: "shareOmitDefaultCss" },
|
||||
{ type: "label", name: "shareRoot" },
|
||||
{ type: "label", name: "shareDescription" },
|
||||
{ type: "label", name: "shareRaw" },
|
||||
{ type: "label", name: "shareDisallowRobotIndexing" },
|
||||
{ type: "label", name: "shareCredentials" },
|
||||
{ type: "label", name: "shareIndex" },
|
||||
{ type: "label", name: "displayRelations" },
|
||||
{ type: "label", name: "hideRelations" },
|
||||
{ type: "label", name: "titleTemplate", isDangerous: true },
|
||||
{ type: "label", name: "template" },
|
||||
{ type: "label", name: "toc" },
|
||||
{ type: "label", name: "color" },
|
||||
{ type: "label", name: "keepCurrentHoisting" },
|
||||
{ type: "label", name: "executeButton" },
|
||||
{ type: "label", name: "executeDescription" },
|
||||
{ type: "label", name: "newNotesOnTop" },
|
||||
{ type: "label", name: "clipperInbox" },
|
||||
{ type: "label", name: "webViewSrc", isDangerous: true },
|
||||
{ type: "label", name: "hideHighlightWidget" },
|
||||
|
||||
// relation names
|
||||
{ type: 'relation', name: 'internalLink' },
|
||||
{ type: 'relation', name: 'imageLink' },
|
||||
{ type: 'relation', name: 'relationMapLink' },
|
||||
{ type: 'relation', name: 'includeMapLink' },
|
||||
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteTitleChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteContentChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnNoteDeletion', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnBranchCreation', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnBranchChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnBranchDeletion', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnChildNoteCreation', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnAttributeCreation', isDangerous: true },
|
||||
{ type: 'relation', name: 'runOnAttributeChange', isDangerous: true },
|
||||
{ type: 'relation', name: 'template' },
|
||||
{ type: 'relation', name: 'inherit' },
|
||||
{ type: 'relation', name: 'widget', isDangerous: true },
|
||||
{ type: 'relation', name: 'renderNote', isDangerous: true },
|
||||
{ type: 'relation', name: 'shareCss' },
|
||||
{ type: 'relation', name: 'shareJs' },
|
||||
{ type: 'relation', name: 'shareTemplate' },
|
||||
{ type: 'relation', name: 'shareFavicon' },
|
||||
{ type: "relation", name: "internalLink" },
|
||||
{ type: "relation", name: "imageLink" },
|
||||
{ type: "relation", name: "relationMapLink" },
|
||||
{ type: "relation", name: "includeMapLink" },
|
||||
{ type: "relation", name: "runOnNoteCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteTitleChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteContentChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteDeletion", isDangerous: true },
|
||||
{ type: "relation", name: "runOnBranchCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnBranchChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnBranchDeletion", isDangerous: true },
|
||||
{ type: "relation", name: "runOnChildNoteCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnAttributeCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnAttributeChange", isDangerous: true },
|
||||
{ type: "relation", name: "template" },
|
||||
{ type: "relation", name: "inherit" },
|
||||
{ type: "relation", name: "widget", isDangerous: true },
|
||||
{ type: "relation", name: "renderNote", isDangerous: true },
|
||||
{ type: "relation", name: "shareCss" },
|
||||
{ type: "relation", name: "shareJs" },
|
||||
{ type: "relation", name: "shareTemplate" },
|
||||
{ type: "relation", name: "shareFavicon" }
|
||||
];
|
||||
|
||||
@@ -12,7 +12,6 @@ interface Action {
|
||||
oldLabelName: string;
|
||||
newLabelName: string;
|
||||
|
||||
|
||||
relationName: string;
|
||||
oldRelationName: string;
|
||||
newRelationName: string;
|
||||
@@ -37,8 +36,9 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
|
||||
note.deleteNote(deleteId);
|
||||
},
|
||||
deleteRevisions: (action, note) => {
|
||||
const revisionIds = note.getRevisions()
|
||||
.map(rev => rev.revisionId)
|
||||
const revisionIds = note
|
||||
.getRevisions()
|
||||
.map((rev) => rev.revisionId)
|
||||
.filter((rev) => !!rev) as string[];
|
||||
eraseService.eraseRevisions(revisionIds);
|
||||
},
|
||||
@@ -66,7 +66,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
|
||||
renameLabel: (action, note) => {
|
||||
for (const label of note.getOwnedLabels(action.oldLabelName)) {
|
||||
// attribute name is immutable, renaming means delete old + create new
|
||||
const newLabel = label.createClone('label', action.newLabelName, label.value);
|
||||
const newLabel = label.createClone("label", action.newLabelName, label.value);
|
||||
|
||||
newLabel.save();
|
||||
label.markAsDeleted();
|
||||
@@ -75,7 +75,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
|
||||
renameRelation: (action, note) => {
|
||||
for (const relation of note.getOwnedRelations(action.oldRelationName)) {
|
||||
// attribute name is immutable, renaming means delete old + create new
|
||||
const newRelation = relation.createClone('relation', action.newRelationName, relation.value);
|
||||
const newRelation = relation.createClone("relation", action.newRelationName, relation.value);
|
||||
|
||||
newRelation.save();
|
||||
relation.markAsDeleted();
|
||||
@@ -106,8 +106,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
|
||||
|
||||
if (note.getParentBranches().length > 1) {
|
||||
res = cloningService.cloneNoteToParentNote(note.noteId, action.targetParentNoteId);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
|
||||
}
|
||||
|
||||
@@ -117,7 +116,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
|
||||
},
|
||||
executeScript: (action, note) => {
|
||||
if (!action.script || !action.script.trim()) {
|
||||
log.info("Ignoring executeScript since the script is empty.")
|
||||
log.info("Ignoring executeScript since the script is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,8 +128,9 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
|
||||
};
|
||||
|
||||
function getActions(note: BNote) {
|
||||
return note.getLabels('action')
|
||||
.map(actionLabel => {
|
||||
return note
|
||||
.getLabels("action")
|
||||
.map((actionLabel) => {
|
||||
let action;
|
||||
|
||||
try {
|
||||
@@ -147,7 +147,7 @@ function getActions(note: BNote) {
|
||||
|
||||
return action;
|
||||
})
|
||||
.filter(a => !!a);
|
||||
.filter((a) => !!a);
|
||||
}
|
||||
|
||||
function executeActions(note: BNote, searchResultNoteIds: string[] | Set<string>) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
import sql from './sql.js';
|
||||
import eventChangesService from './entity_changes.js';
|
||||
import treeService from './tree.js';
|
||||
import BBranch from '../becca/entities/bbranch.js';
|
||||
import becca from '../becca/becca.js';
|
||||
import log from './log.js';
|
||||
import sql from "./sql.js";
|
||||
import eventChangesService from "./entity_changes.js";
|
||||
import treeService from "./tree.js";
|
||||
import BBranch from "../becca/entities/bbranch.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import log from "./log.js";
|
||||
|
||||
export interface CloneResponse {
|
||||
success: boolean;
|
||||
@@ -16,15 +16,15 @@ export interface CloneResponse {
|
||||
|
||||
function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: string | null = null): CloneResponse {
|
||||
if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) {
|
||||
return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' };
|
||||
return { success: false, message: "Note cannot be cloned because either the cloned note or the intended parent is deleted." };
|
||||
}
|
||||
|
||||
const parentNote = becca.getNote(parentNoteId);
|
||||
if (!parentNote) {
|
||||
return { success: false, message: 'Note cannot be cloned because the parent note could not be found.' };
|
||||
return { success: false, message: "Note cannot be cloned because the parent note could not be found." };
|
||||
}
|
||||
|
||||
if (parentNote.type === 'search') {
|
||||
if (parentNote.type === "search") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Can't clone into a search note"
|
||||
@@ -80,7 +80,7 @@ function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefi
|
||||
if (!parentNote) {
|
||||
return { branch: null, success: false, message: "Can't find parent note." };
|
||||
}
|
||||
if (parentNote.type === 'search') {
|
||||
if (parentNote.type === "search") {
|
||||
return { branch: null, success: false, message: "Can't clone into a search note" };
|
||||
}
|
||||
|
||||
@@ -125,14 +125,13 @@ function ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string) {
|
||||
function toggleNoteInParent(present: boolean, noteId: string, parentNoteId: string, prefix?: string) {
|
||||
if (present) {
|
||||
return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return ensureNoteIsAbsentFromParent(noteId, parentNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
function cloneNoteAfter(noteId: string, afterBranchId: string) {
|
||||
if (['_hidden', 'root'].includes(noteId)) {
|
||||
if (["_hidden", "root"].includes(noteId)) {
|
||||
return { success: false, message: `Cloning the note '${noteId}' is forbidden.` };
|
||||
}
|
||||
|
||||
@@ -142,8 +141,8 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) {
|
||||
return { success: false, message: `Branch '${afterBranchId}' does not exist.` };
|
||||
}
|
||||
|
||||
if (afterBranch.noteId === '_hidden') {
|
||||
return { success: false, message: 'Cannot clone after the hidden branch.' };
|
||||
if (afterBranch.noteId === "_hidden") {
|
||||
return { success: false, message: "Cannot clone after the hidden branch." };
|
||||
}
|
||||
|
||||
const afterNote = becca.getBranch(afterBranchId);
|
||||
@@ -156,7 +155,7 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) {
|
||||
|
||||
const parentNote = becca.getNote(afterNote.parentNoteId);
|
||||
|
||||
if (!parentNote || parentNote.type === 'search') {
|
||||
if (!parentNote || parentNote.type === "search") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Can't clone into a search note"
|
||||
@@ -171,8 +170,7 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) {
|
||||
|
||||
// we don't change utcDateModified, so other changes are prioritized in case of conflict
|
||||
// also we would have to sync all those modified branches otherwise hash checks would fail
|
||||
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
|
||||
[afterNote.parentNoteId, afterNote.notePosition]);
|
||||
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, afterNote.notePosition]);
|
||||
|
||||
eventChangesService.putNoteReorderingEntityChange(afterNote.parentNoteId);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsHooked from "cls-hooked";
|
||||
import { EntityChange } from './entity_changes_interface.js';
|
||||
import { EntityChange } from "./entity_changes_interface.js";
|
||||
const namespace = clsHooked.createNamespace("trilium");
|
||||
|
||||
type Callback = (...args: any[]) => any;
|
||||
@@ -12,11 +12,10 @@ function wrap(callback: Callback) {
|
||||
return () => {
|
||||
try {
|
||||
init(callback);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
console.log(`Error occurred: ${e.message}: ${e.stack}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function get(key: string) {
|
||||
@@ -28,64 +27,64 @@ function set(key: string, value: any) {
|
||||
}
|
||||
|
||||
function getHoistedNoteId() {
|
||||
return namespace.get('hoistedNoteId') || 'root';
|
||||
return namespace.get("hoistedNoteId") || "root";
|
||||
}
|
||||
|
||||
function getComponentId() {
|
||||
return namespace.get('componentId');
|
||||
return namespace.get("componentId");
|
||||
}
|
||||
|
||||
function getLocalNowDateTime() {
|
||||
return namespace.get('localNowDateTime');
|
||||
return namespace.get("localNowDateTime");
|
||||
}
|
||||
|
||||
function disableEntityEvents() {
|
||||
namespace.set('disableEntityEvents', true);
|
||||
namespace.set("disableEntityEvents", true);
|
||||
}
|
||||
|
||||
function enableEntityEvents() {
|
||||
namespace.set('disableEntityEvents', false);
|
||||
namespace.set("disableEntityEvents", false);
|
||||
}
|
||||
|
||||
function isEntityEventsDisabled() {
|
||||
return !!namespace.get('disableEntityEvents');
|
||||
return !!namespace.get("disableEntityEvents");
|
||||
}
|
||||
|
||||
function setMigrationRunning(running: boolean) {
|
||||
namespace.set('migrationRunning', !!running);
|
||||
namespace.set("migrationRunning", !!running);
|
||||
}
|
||||
|
||||
function isMigrationRunning() {
|
||||
return !!namespace.get('migrationRunning');
|
||||
return !!namespace.get("migrationRunning");
|
||||
}
|
||||
|
||||
function disableSlowQueryLogging(disable: boolean) {
|
||||
namespace.set('disableSlowQueryLogging', disable);
|
||||
namespace.set("disableSlowQueryLogging", disable);
|
||||
}
|
||||
|
||||
function isSlowQueryLoggingDisabled() {
|
||||
return !!namespace.get('disableSlowQueryLogging');
|
||||
return !!namespace.get("disableSlowQueryLogging");
|
||||
}
|
||||
|
||||
function getAndClearEntityChangeIds() {
|
||||
const entityChangeIds = namespace.get('entityChangeIds') || [];
|
||||
const entityChangeIds = namespace.get("entityChangeIds") || [];
|
||||
|
||||
namespace.set('entityChangeIds', []);
|
||||
namespace.set("entityChangeIds", []);
|
||||
|
||||
return entityChangeIds;
|
||||
}
|
||||
|
||||
function putEntityChange(entityChange: EntityChange) {
|
||||
if (namespace.get('ignoreEntityChangeIds')) {
|
||||
if (namespace.get("ignoreEntityChangeIds")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityChangeIds = namespace.get('entityChangeIds') || [];
|
||||
const entityChangeIds = namespace.get("entityChangeIds") || [];
|
||||
|
||||
// store only ID since the record can be modified (e.g., in erase)
|
||||
entityChangeIds.push(entityChange.id);
|
||||
|
||||
namespace.set('entityChangeIds', entityChangeIds);
|
||||
namespace.set("entityChangeIds", entityChangeIds);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
@@ -93,7 +92,7 @@ function reset() {
|
||||
}
|
||||
|
||||
function ignoreEntityChangeIds() {
|
||||
namespace.set('ignoreEntityChangeIds', true);
|
||||
namespace.set("ignoreEntityChangeIds", true);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import themeNames from "./code_block_theme_names.json" with { type: "json" }
|
||||
import themeNames from "./code_block_theme_names.json" with { type: "json" };
|
||||
import { t } from "i18next";
|
||||
import { join } from "path";
|
||||
import { isElectron, getResourceDir } from "./utils.js";
|
||||
@@ -42,7 +42,7 @@ export function listSyntaxHighlightingThemes() {
|
||||
}
|
||||
],
|
||||
...groupThemesByLightOrDark(systemThemes)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getStylesDirectory() {
|
||||
@@ -62,7 +62,8 @@ function getStylesDirectory() {
|
||||
* @returns the list of themes.
|
||||
*/
|
||||
function readThemesFromFileSystem(path: string): ColorTheme[] {
|
||||
return fs.readdirSync(path)
|
||||
return fs
|
||||
.readdirSync(path)
|
||||
.filter((el) => el.endsWith(".min.css"))
|
||||
.map((name) => {
|
||||
const nameWithoutExtension = name.replace(".min.css", "");
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
{
|
||||
"1c light": "1C (Light)",
|
||||
"a11y dark": "a11y (Dark)",
|
||||
"a11y light": "a11y (Light)",
|
||||
"agate": "Agate (Dark)",
|
||||
"an old hope": "An Old Hope (Dark)",
|
||||
"androidstudio": "Android Studio (Dark)",
|
||||
"arduino light": "Arduino (Light)",
|
||||
"arta": "Arta (Dark)",
|
||||
"ascetic": "Ascetic (Light)",
|
||||
"atom one dark reasonable": "Atom One with ReasonML support (Dark)",
|
||||
"atom one dark": "Atom One (Dark)",
|
||||
"atom one light": "Atom One (Light)",
|
||||
"brown paper": "Brown Paper (Light)",
|
||||
"codepen embed": "CodePen Embed (Dark)",
|
||||
"color brewer": "Color Brewer (Light)",
|
||||
"dark": "Dark",
|
||||
"default": "Original highlight.js Theme (Light)",
|
||||
"devibeans": "devibeans (Dark)",
|
||||
"docco": "Docco (Light)",
|
||||
"far": "FAR (Dark)",
|
||||
"felipec": "FelipeC (Dark)",
|
||||
"foundation": "Foundation 4 Docs (Light)",
|
||||
"github dark dimmed": "GitHub Dimmed (Dark)",
|
||||
"github dark": "GitHub (Dark)",
|
||||
"github": "GitHub (Light)",
|
||||
"gml": "GML (Dark)",
|
||||
"googlecode": "Google Code (Light)",
|
||||
"gradient dark": "Gradient (Dark)",
|
||||
"gradient light": "Gradient (Light)",
|
||||
"grayscale": "Grayscale (Light)",
|
||||
"hybrid": "hybrid (Dark)",
|
||||
"idea": "Idea (Light)",
|
||||
"intellij light": "IntelliJ (Light)",
|
||||
"ir black": "IR Black (Dark)",
|
||||
"isbl editor dark": "ISBL Editor (Dark)",
|
||||
"isbl editor light": "ISBL Editor (Light)",
|
||||
"kimbie dark": "Kimbie (Dark)",
|
||||
"kimbie light": "Kimbie (Light)",
|
||||
"lightfair": "Lightfair (Light)",
|
||||
"lioshi": "Lioshi (Dark)",
|
||||
"magula": "Magula (Light)",
|
||||
"mono blue": "Mono Blue (Light)",
|
||||
"monokai sublime": "Monokai Sublime (Dark)",
|
||||
"monokai": "Monokai (Dark)",
|
||||
"night owl": "Night Owl (Dark)",
|
||||
"nnfx dark": "NNFX (Dark)",
|
||||
"nnfx light": "NNFX (Light)",
|
||||
"nord": "Nord (Dark)",
|
||||
"obsidian": "Obsidian (Dark)",
|
||||
"panda syntax dark": "Panda (Dark)",
|
||||
"panda syntax light": "Panda (Light)",
|
||||
"paraiso dark": "Paraiso (Dark)",
|
||||
"paraiso light": "Paraiso (Light)",
|
||||
"pojoaque": "Pojoaque (Dark)",
|
||||
"purebasic": "PureBasic (Light)",
|
||||
"qtcreator dark": "Qt Creator (Dark)",
|
||||
"qtcreator light": "Qt Creator (Light)",
|
||||
"rainbow": "Rainbow (Dark)",
|
||||
"routeros": "RouterOS Script (Light)",
|
||||
"school book": "School Book (Light)",
|
||||
"shades of purple": "Shades of Purple (Dark)",
|
||||
"srcery": "Srcery (Dark)",
|
||||
"stackoverflow dark": "Stack Overflow (Dark)",
|
||||
"stackoverflow light": "Stack Overflow (Light)",
|
||||
"sunburst": "Sunburst (Dark)",
|
||||
"tokyo night dark": "Tokyo Night (Dark)",
|
||||
"tokyo night light": "Tokyo Night (Light)",
|
||||
"tomorrow night blue": "Tomorrow Night Blue (Dark)",
|
||||
"tomorrow night bright": "Tomorrow Night Bright (Dark)",
|
||||
"vs": "Visual Studio (Light)",
|
||||
"vs2015": "Visual Studio 2015 (Dark)",
|
||||
"xcode": "Xcode (Light)",
|
||||
"xt256": "xt256 (Dark)"
|
||||
"1c light": "1C (Light)",
|
||||
"a11y dark": "a11y (Dark)",
|
||||
"a11y light": "a11y (Light)",
|
||||
"agate": "Agate (Dark)",
|
||||
"an old hope": "An Old Hope (Dark)",
|
||||
"androidstudio": "Android Studio (Dark)",
|
||||
"arduino light": "Arduino (Light)",
|
||||
"arta": "Arta (Dark)",
|
||||
"ascetic": "Ascetic (Light)",
|
||||
"atom one dark reasonable": "Atom One with ReasonML support (Dark)",
|
||||
"atom one dark": "Atom One (Dark)",
|
||||
"atom one light": "Atom One (Light)",
|
||||
"brown paper": "Brown Paper (Light)",
|
||||
"codepen embed": "CodePen Embed (Dark)",
|
||||
"color brewer": "Color Brewer (Light)",
|
||||
"dark": "Dark",
|
||||
"default": "Original highlight.js Theme (Light)",
|
||||
"devibeans": "devibeans (Dark)",
|
||||
"docco": "Docco (Light)",
|
||||
"far": "FAR (Dark)",
|
||||
"felipec": "FelipeC (Dark)",
|
||||
"foundation": "Foundation 4 Docs (Light)",
|
||||
"github dark dimmed": "GitHub Dimmed (Dark)",
|
||||
"github dark": "GitHub (Dark)",
|
||||
"github": "GitHub (Light)",
|
||||
"gml": "GML (Dark)",
|
||||
"googlecode": "Google Code (Light)",
|
||||
"gradient dark": "Gradient (Dark)",
|
||||
"gradient light": "Gradient (Light)",
|
||||
"grayscale": "Grayscale (Light)",
|
||||
"hybrid": "hybrid (Dark)",
|
||||
"idea": "Idea (Light)",
|
||||
"intellij light": "IntelliJ (Light)",
|
||||
"ir black": "IR Black (Dark)",
|
||||
"isbl editor dark": "ISBL Editor (Dark)",
|
||||
"isbl editor light": "ISBL Editor (Light)",
|
||||
"kimbie dark": "Kimbie (Dark)",
|
||||
"kimbie light": "Kimbie (Light)",
|
||||
"lightfair": "Lightfair (Light)",
|
||||
"lioshi": "Lioshi (Dark)",
|
||||
"magula": "Magula (Light)",
|
||||
"mono blue": "Mono Blue (Light)",
|
||||
"monokai sublime": "Monokai Sublime (Dark)",
|
||||
"monokai": "Monokai (Dark)",
|
||||
"night owl": "Night Owl (Dark)",
|
||||
"nnfx dark": "NNFX (Dark)",
|
||||
"nnfx light": "NNFX (Light)",
|
||||
"nord": "Nord (Dark)",
|
||||
"obsidian": "Obsidian (Dark)",
|
||||
"panda syntax dark": "Panda (Dark)",
|
||||
"panda syntax light": "Panda (Light)",
|
||||
"paraiso dark": "Paraiso (Dark)",
|
||||
"paraiso light": "Paraiso (Light)",
|
||||
"pojoaque": "Pojoaque (Dark)",
|
||||
"purebasic": "PureBasic (Light)",
|
||||
"qtcreator dark": "Qt Creator (Dark)",
|
||||
"qtcreator light": "Qt Creator (Light)",
|
||||
"rainbow": "Rainbow (Dark)",
|
||||
"routeros": "RouterOS Script (Light)",
|
||||
"school book": "School Book (Light)",
|
||||
"shades of purple": "Shades of Purple (Dark)",
|
||||
"srcery": "Srcery (Dark)",
|
||||
"stackoverflow dark": "Stack Overflow (Dark)",
|
||||
"stackoverflow light": "Stack Overflow (Light)",
|
||||
"sunburst": "Sunburst (Dark)",
|
||||
"tokyo night dark": "Tokyo Night (Dark)",
|
||||
"tokyo night light": "Tokyo Night (Light)",
|
||||
"tomorrow night blue": "Tomorrow Night Blue (Dark)",
|
||||
"tomorrow night bright": "Tomorrow Night Bright (Dark)",
|
||||
"vs": "Visual Studio (Light)",
|
||||
"vs2015": "Visual Studio 2015 (Dark)",
|
||||
"xcode": "Xcode (Light)",
|
||||
"xt256": "xt256 (Dark)"
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import resourceDir from "./resource_dir.js";
|
||||
const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini");
|
||||
|
||||
if (!fs.existsSync(dataDir.CONFIG_INI_PATH)) {
|
||||
const configSample = fs.readFileSync(configSampleFilePath).toString('utf8');
|
||||
const configSample = fs.readFileSync(configSampleFilePath).toString("utf8");
|
||||
|
||||
fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample);
|
||||
}
|
||||
|
||||
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8'));
|
||||
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -14,21 +14,20 @@ import { hash as getHash, hashedBlobId, randomString } from "../services/utils.j
|
||||
import eraseService from "../services/erase.js";
|
||||
import sanitizeAttributeName from "./sanitize_attribute_name.js";
|
||||
import noteTypesService from "../services/note_types.js";
|
||||
import { BranchRow } from '../becca/entities/rows.js';
|
||||
import { EntityChange } from './entity_changes_interface.js';
|
||||
import { BranchRow } from "../becca/entities/rows.js";
|
||||
import { EntityChange } from "./entity_changes_interface.js";
|
||||
import becca_loader from "../becca/becca_loader.js";
|
||||
const noteTypes = noteTypesService.getNoteTypeNames();
|
||||
|
||||
class ConsistencyChecks {
|
||||
|
||||
private autoFix: boolean;
|
||||
private unrecoveredConsistencyErrors: boolean;
|
||||
private fixedIssues: boolean;
|
||||
private reloadNeeded: boolean;
|
||||
|
||||
/**
|
||||
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
|
||||
*/
|
||||
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
|
||||
*/
|
||||
constructor(autoFix: boolean) {
|
||||
this.autoFix = autoFix;
|
||||
this.unrecoveredConsistencyErrors = false;
|
||||
@@ -71,7 +70,7 @@ class ConsistencyChecks {
|
||||
|
||||
/** @returns true if cycle was found and we should try again */
|
||||
const checkTreeCycle = (noteId: string, path: string[]) => {
|
||||
if (noteId === 'root') {
|
||||
if (noteId === "root") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -80,13 +79,12 @@ class ConsistencyChecks {
|
||||
if (this.autoFix) {
|
||||
const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId);
|
||||
if (branch) {
|
||||
branch.markAsDeleted('cycle-autofix');
|
||||
branch.markAsDeleted("cycle-autofix");
|
||||
logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
logError(`Tree cycle detected at parent-child relationship: '${parentNoteId}' - '${noteId}', whole path: '${path}'`);
|
||||
|
||||
this.unrecoveredConsistencyErrors = true;
|
||||
@@ -135,13 +133,14 @@ class ConsistencyChecks {
|
||||
}
|
||||
|
||||
findBrokenReferenceIssues() {
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT branchId, branches.noteId
|
||||
FROM branches
|
||||
LEFT JOIN notes USING (noteId)
|
||||
WHERE branches.isDeleted = 0
|
||||
AND notes.noteId IS NULL`,
|
||||
({branchId, noteId}) => {
|
||||
({ branchId, noteId }) => {
|
||||
if (this.autoFix) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
if (!branch) {
|
||||
@@ -155,16 +154,18 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Branch '${branchId}' references missing note '${noteId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT branchId, branches.parentNoteId AS parentNoteId
|
||||
FROM branches
|
||||
LEFT JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE branches.isDeleted = 0
|
||||
AND branches.noteId != 'root'
|
||||
AND notes.noteId IS NULL`,
|
||||
({branchId, parentNoteId}) => {
|
||||
({ branchId, parentNoteId }) => {
|
||||
if (this.autoFix) {
|
||||
// Delete the old branch and recreate it with root as parent.
|
||||
const oldBranch = becca.getBranch(branchId);
|
||||
@@ -184,9 +185,9 @@ class ConsistencyChecks {
|
||||
|
||||
if (note.getParentBranches().length === 0) {
|
||||
const newBranch = new BBranch({
|
||||
parentNoteId: 'root',
|
||||
parentNoteId: "root",
|
||||
noteId: noteId,
|
||||
prefix: 'recovered'
|
||||
prefix: "recovered"
|
||||
}).save();
|
||||
|
||||
message += `${newBranch.branchId} was created in the root instead.`;
|
||||
@@ -200,15 +201,17 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Branch '${branchId}' references missing parent note '${parentNoteId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attributeId, attributes.noteId
|
||||
FROM attributes
|
||||
LEFT JOIN notes USING (noteId)
|
||||
WHERE attributes.isDeleted = 0
|
||||
AND notes.noteId IS NULL`,
|
||||
({attributeId, noteId}) => {
|
||||
({ attributeId, noteId }) => {
|
||||
if (this.autoFix) {
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
if (!attribute) {
|
||||
@@ -222,16 +225,18 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Attribute '${attributeId}' references missing source note '${noteId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attributeId, attributes.value AS noteId
|
||||
FROM attributes
|
||||
LEFT JOIN notes ON notes.noteId = attributes.value
|
||||
WHERE attributes.isDeleted = 0
|
||||
AND attributes.type = 'relation'
|
||||
AND notes.noteId IS NULL`,
|
||||
({attributeId, noteId}) => {
|
||||
({ attributeId, noteId }) => {
|
||||
if (this.autoFix) {
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
if (!attribute) {
|
||||
@@ -241,13 +246,15 @@ class ConsistencyChecks {
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Relation '${attributeId}' has been deleted since it references missing note '${noteId}'`)
|
||||
logFix(`Relation '${attributeId}' has been deleted since it references missing note '${noteId}'`);
|
||||
} else {
|
||||
logError(`Relation '${attributeId}' references missing note '${noteId}'`)
|
||||
logError(`Relation '${attributeId}' references missing note '${noteId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attachmentId, attachments.ownerId AS noteId
|
||||
FROM attachments
|
||||
WHERE attachments.ownerId NOT IN (
|
||||
@@ -256,7 +263,7 @@ class ConsistencyChecks {
|
||||
SELECT revisionId FROM revisions
|
||||
)
|
||||
AND attachments.isDeleted = 0`,
|
||||
({attachmentId, ownerId}) => {
|
||||
({ attachmentId, ownerId }) => {
|
||||
if (this.autoFix) {
|
||||
const attachment = becca.getAttachment(attachmentId);
|
||||
if (!attachment) {
|
||||
@@ -270,7 +277,8 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Attachment '${attachmentId}' references missing note/revision '${ownerId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
findExistencyIssues() {
|
||||
@@ -280,14 +288,15 @@ class ConsistencyChecks {
|
||||
|
||||
// the order here is important - first we might need to delete inconsistent branches, and after that
|
||||
// another check might create missing branch
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT branchId,
|
||||
noteId
|
||||
FROM branches
|
||||
JOIN notes USING (noteId)
|
||||
WHERE notes.isDeleted = 1
|
||||
AND branches.isDeleted = 0`,
|
||||
({branchId, noteId}) => {
|
||||
({ branchId, noteId }) => {
|
||||
if (this.autoFix) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
if (!branch) return;
|
||||
@@ -297,57 +306,65 @@ class ConsistencyChecks {
|
||||
|
||||
logFix(`Branch '${branchId}' has been deleted since the associated note '${noteId}' is deleted.`);
|
||||
} else {
|
||||
logError(`Branch '${branchId}' is not deleted even though the associated note '${noteId}' is deleted.`)
|
||||
logError(`Branch '${branchId}' is not deleted even though the associated note '${noteId}' is deleted.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT branchId,
|
||||
parentNoteId
|
||||
FROM branches
|
||||
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
|
||||
WHERE parentNote.isDeleted = 1
|
||||
AND branches.isDeleted = 0
|
||||
`, ({branchId, parentNoteId}) => {
|
||||
if (this.autoFix) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
if (!branch) {
|
||||
return;
|
||||
`,
|
||||
({ branchId, parentNoteId }) => {
|
||||
if (this.autoFix) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
if (!branch) {
|
||||
return;
|
||||
}
|
||||
branch.markAsDeleted();
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Branch '${branchId}' has been deleted since the associated parent note '${parentNoteId}' is deleted.`);
|
||||
} else {
|
||||
logError(`Branch '${branchId}' is not deleted even though the associated parent note '${parentNoteId}' is deleted.`);
|
||||
}
|
||||
branch.markAsDeleted();
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Branch '${branchId}' has been deleted since the associated parent note '${parentNoteId}' is deleted.`);
|
||||
} else {
|
||||
logError(`Branch '${branchId}' is not deleted even though the associated parent note '${parentNoteId}' is deleted.`)
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT DISTINCT notes.noteId
|
||||
FROM notes
|
||||
LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0
|
||||
WHERE notes.isDeleted = 0
|
||||
AND branches.branchId IS NULL
|
||||
`, ({noteId}) => {
|
||||
if (this.autoFix) {
|
||||
const branch = new BBranch({
|
||||
parentNoteId: 'root',
|
||||
noteId: noteId,
|
||||
prefix: 'recovered'
|
||||
}).save();
|
||||
`,
|
||||
({ noteId }) => {
|
||||
if (this.autoFix) {
|
||||
const branch = new BBranch({
|
||||
parentNoteId: "root",
|
||||
noteId: noteId,
|
||||
prefix: "recovered"
|
||||
}).save();
|
||||
|
||||
this.reloadNeeded = true;
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Created missing branch '${branch.branchId}' for note '${noteId}'`);
|
||||
} else {
|
||||
logError(`No undeleted branch found for note '${noteId}'`);
|
||||
logFix(`Created missing branch '${branch.branchId}' for note '${noteId}'`);
|
||||
} else {
|
||||
logError(`No undeleted branch found for note '${noteId}'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// there should be a unique relationship between note and its parent
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT noteId,
|
||||
parentNoteId
|
||||
FROM branches
|
||||
@@ -355,17 +372,19 @@ class ConsistencyChecks {
|
||||
GROUP BY branches.parentNoteId,
|
||||
branches.noteId
|
||||
HAVING COUNT(1) > 1`,
|
||||
({noteId, parentNoteId}) => {
|
||||
({ noteId, parentNoteId }) => {
|
||||
if (this.autoFix) {
|
||||
const branchIds = sql.getColumn<string>(
|
||||
`SELECT branchId
|
||||
`SELECT branchId
|
||||
FROM branches
|
||||
WHERE noteId = ?
|
||||
and parentNoteId = ?
|
||||
and isDeleted = 0
|
||||
ORDER BY utcDateModified`, [noteId, parentNoteId]);
|
||||
ORDER BY utcDateModified`,
|
||||
[noteId, parentNoteId]
|
||||
);
|
||||
|
||||
const branches = branchIds.map(branchId => becca.getBranch(branchId));
|
||||
const branches = branchIds.map((branchId) => becca.getBranch(branchId));
|
||||
|
||||
// it's not necessarily "original" branch, it's just the only one which will survive
|
||||
const origBranch = branches[0];
|
||||
@@ -389,16 +408,18 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Duplicate branches for note '${noteId}' and parent '${parentNoteId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attachmentId,
|
||||
attachments.ownerId AS noteId
|
||||
FROM attachments
|
||||
JOIN notes ON notes.noteId = attachments.ownerId
|
||||
WHERE notes.isDeleted = 1
|
||||
AND attachments.isDeleted = 0`,
|
||||
({attachmentId, noteId}) => {
|
||||
({ attachmentId, noteId }) => {
|
||||
if (this.autoFix) {
|
||||
const attachment = becca.getAttachment(attachmentId);
|
||||
if (!attachment) return;
|
||||
@@ -408,41 +429,45 @@ class ConsistencyChecks {
|
||||
|
||||
logFix(`Attachment '${attachmentId}' has been deleted since the associated note '${noteId}' is deleted.`);
|
||||
} else {
|
||||
logError(`Attachment '${attachmentId}' is not deleted even though the associated note '${noteId}' is deleted.`)
|
||||
logError(`Attachment '${attachmentId}' is not deleted even though the associated note '${noteId}' is deleted.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
findLogicIssues() {
|
||||
const noteTypesStr = noteTypes.map(nt => `'${nt}'`).join(", ");
|
||||
const noteTypesStr = noteTypes.map((nt) => `'${nt}'`).join(", ");
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT noteId, type
|
||||
FROM notes
|
||||
WHERE isDeleted = 0
|
||||
AND type NOT IN (${noteTypesStr})`,
|
||||
({noteId, type}) => {
|
||||
({ noteId, type }) => {
|
||||
if (this.autoFix) {
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note) return;
|
||||
note.type = 'file'; // file is a safe option to recover notes if the type is not known
|
||||
note.type = "file"; // file is a safe option to recover notes if the type is not known
|
||||
note.save();
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Note '${noteId}' type has been change to file since it had invalid type '${type}'`)
|
||||
logFix(`Note '${noteId}' type has been change to file since it had invalid type '${type}'`);
|
||||
} else {
|
||||
logError(`Note '${noteId}' has invalid type '${type}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
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}) => {
|
||||
({ noteId, isProtected, type, mime }) => {
|
||||
if (this.autoFix) {
|
||||
// it might be possible that the blob is not available only because of the interrupted
|
||||
// sync, and it will come later. It's therefore important to guarantee that this artificial
|
||||
@@ -469,7 +494,7 @@ class ConsistencyChecks {
|
||||
const hash = getHash(randomString(10));
|
||||
|
||||
entityChangesService.putEntityChange({
|
||||
entityName: 'blobs',
|
||||
entityName: "blobs",
|
||||
entityId: blobId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
@@ -486,19 +511,21 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Note '${noteId}' content row does not exist`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
|
||||
this.findAndFixIssues(`
|
||||
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}) => {
|
||||
({ noteId, type, mime }) => {
|
||||
if (this.autoFix) {
|
||||
const note = becca.getNote(noteId);
|
||||
const blankContent = getBlankContent(false, type, mime);
|
||||
@@ -514,15 +541,17 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Note '${noteId}' content is null even though it is not deleted`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT revisions.revisionId, blobs.blobId
|
||||
FROM revisions
|
||||
LEFT JOIN blobs USING (blobId)
|
||||
WHERE blobs.blobId IS NULL`,
|
||||
({revisionId, blobId}) => {
|
||||
({ revisionId, blobId }) => {
|
||||
if (this.autoFix) {
|
||||
eraseService.eraseRevisions([revisionId]);
|
||||
|
||||
@@ -532,14 +561,16 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Note revision '${revisionId}' blob '${blobId}' does not exist`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attachments.attachmentId, blobs.blobId
|
||||
FROM attachments
|
||||
LEFT JOIN blobs USING (blobId)
|
||||
WHERE blobs.blobId IS NULL`,
|
||||
({attachmentId, blobId}) => {
|
||||
({ attachmentId, blobId }) => {
|
||||
if (this.autoFix) {
|
||||
eraseService.eraseAttachments([attachmentId]);
|
||||
|
||||
@@ -549,24 +580,29 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Attachment '${attachmentId}' blob '${blobId}' does not exist`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT parentNoteId
|
||||
FROM branches
|
||||
JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE notes.isDeleted = 0
|
||||
AND notes.type == 'search'
|
||||
AND branches.isDeleted = 0`,
|
||||
({parentNoteId}) => {
|
||||
({ parentNoteId }) => {
|
||||
if (this.autoFix) {
|
||||
const branchIds = sql.getColumn<string>(`
|
||||
const branchIds = sql.getColumn<string>(
|
||||
`
|
||||
SELECT branchId
|
||||
FROM branches
|
||||
WHERE isDeleted = 0
|
||||
AND parentNoteId = ?`, [parentNoteId]);
|
||||
AND parentNoteId = ?`,
|
||||
[parentNoteId]
|
||||
);
|
||||
|
||||
const branches = branchIds.map(branchId => becca.getBranch(branchId));
|
||||
const branches = branchIds.map((branchId) => becca.getBranch(branchId));
|
||||
|
||||
for (const branch of branches) {
|
||||
if (!branch) continue;
|
||||
@@ -576,27 +612,29 @@ class ConsistencyChecks {
|
||||
|
||||
// create a replacement branch in root parent
|
||||
new BBranch({
|
||||
parentNoteId: 'root',
|
||||
parentNoteId: "root",
|
||||
noteId: branch.noteId,
|
||||
prefix: 'recovered'
|
||||
prefix: "recovered"
|
||||
}).save();
|
||||
|
||||
logFix(`Note '${branch.noteId}' has been moved to root since it was a child of a search note '${parentNoteId}'`)
|
||||
logFix(`Note '${branch.noteId}' has been moved to root since it was a child of a search note '${parentNoteId}'`);
|
||||
}
|
||||
|
||||
this.reloadNeeded = true;
|
||||
} else {
|
||||
logError(`Search note '${parentNoteId}' has children`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attributeId
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND type = 'relation'
|
||||
AND value = ''`,
|
||||
({attributeId}) => {
|
||||
({ attributeId }) => {
|
||||
if (this.autoFix) {
|
||||
const relation = becca.getAttribute(attributeId);
|
||||
if (!relation) return;
|
||||
@@ -608,20 +646,22 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Relation '${attributeId}' has empty target.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attributeId,
|
||||
type
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND type != 'label'
|
||||
AND type != 'relation'`,
|
||||
({attributeId, type}) => {
|
||||
({ attributeId, type }) => {
|
||||
if (this.autoFix) {
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
if (!attribute) return;
|
||||
attribute.type = 'label';
|
||||
attribute.type = "label";
|
||||
attribute.save();
|
||||
|
||||
this.reloadNeeded = true;
|
||||
@@ -630,16 +670,18 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Attribute '${attributeId}' has invalid type '${type}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attributeId,
|
||||
attributes.noteId
|
||||
FROM attributes
|
||||
JOIN notes ON attributes.noteId = notes.noteId
|
||||
WHERE attributes.isDeleted = 0
|
||||
AND notes.isDeleted = 1`,
|
||||
({attributeId, noteId}) => {
|
||||
({ attributeId, noteId }) => {
|
||||
if (this.autoFix) {
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
if (!attribute) return;
|
||||
@@ -651,9 +693,11 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Attribute '${attributeId}' is not deleted even though owning note '${noteId}' is deleted.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT attributeId,
|
||||
attributes.value AS targetNoteId
|
||||
FROM attributes
|
||||
@@ -661,7 +705,7 @@ class ConsistencyChecks {
|
||||
WHERE attributes.type = 'relation'
|
||||
AND attributes.isDeleted = 0
|
||||
AND notes.isDeleted = 1`,
|
||||
({attributeId, targetNoteId}) => {
|
||||
({ attributeId, targetNoteId }) => {
|
||||
if (this.autoFix) {
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
if (!attribute) return;
|
||||
@@ -673,16 +717,18 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Attribute '${attributeId}' is not deleted even though target note '${targetNoteId}' is deleted.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
runEntityChangeChecks(entityName: string, key: string) {
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
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`,
|
||||
({entityId}) => {
|
||||
({ entityId }) => {
|
||||
const entityRow = sql.getRow<EntityChange>(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
|
||||
|
||||
if (this.autoFix) {
|
||||
@@ -692,16 +738,18 @@ class ConsistencyChecks {
|
||||
hash: randomString(10), // doesn't matter, will force sync, but that's OK
|
||||
isErased: false,
|
||||
utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated,
|
||||
isSynced: entityName !== 'options' || entityRow.isSynced
|
||||
isSynced: entityName !== "options" || entityRow.isSynced
|
||||
});
|
||||
|
||||
logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`);
|
||||
} else {
|
||||
logError(`Missing entity change for entityName '${entityName}', entityId '${entityId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT id, entityId
|
||||
FROM entity_changes
|
||||
LEFT JOIN ${entityName} ON entityId = ${entityName}.${key}
|
||||
@@ -709,24 +757,26 @@ class ConsistencyChecks {
|
||||
entity_changes.isErased = 0
|
||||
AND entity_changes.entityName = '${entityName}'
|
||||
AND ${entityName}.${key} IS NULL`,
|
||||
({id, entityId}) => {
|
||||
if (this.autoFix) {
|
||||
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
|
||||
({ id, entityId }) => {
|
||||
if (this.autoFix) {
|
||||
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
|
||||
|
||||
logFix(`Deleted extra entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
|
||||
} else {
|
||||
logError(`Unrecognized entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
|
||||
}
|
||||
});
|
||||
logFix(`Deleted extra entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
|
||||
} else {
|
||||
logError(`Unrecognized entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.findAndFixIssues(`
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
SELECT id, entityId
|
||||
FROM entity_changes
|
||||
JOIN ${entityName} ON entityId = ${entityName}.${key}
|
||||
WHERE
|
||||
entity_changes.isErased = 1
|
||||
AND entity_changes.entityName = '${entityName}'`,
|
||||
({id, entityId}) => {
|
||||
({ id, entityId }) => {
|
||||
if (this.autoFix) {
|
||||
sql.execute(`DELETE FROM ${entityName} WHERE ${key} = ?`, [entityId]);
|
||||
|
||||
@@ -736,7 +786,8 @@ class ConsistencyChecks {
|
||||
} else {
|
||||
logError(`Entity change id '${id}' has entityName '${entityName}', entityId '${entityId}' as erased, but it's not.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
findEntityChangeIssues() {
|
||||
@@ -763,14 +814,13 @@ class ConsistencyChecks {
|
||||
// - 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)
|
||||
// But in general, we assume there won't be many such problems
|
||||
sql.execute('UPDATE attributes SET name = ? WHERE name = ?', [fixedName, origName]);
|
||||
sql.execute("UPDATE attributes SET name = ? WHERE name = ?", [fixedName, origName]);
|
||||
|
||||
this.fixedIssues = true;
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Renamed incorrectly named attributes '${origName}' to '${fixedName}'`);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.unrecoveredConsistencyErrors = true;
|
||||
|
||||
logFix(`There are incorrectly named attributes '${origName}'`);
|
||||
@@ -790,8 +840,7 @@ class ConsistencyChecks {
|
||||
this.fixedIssues = true;
|
||||
|
||||
logFix(`Fixed incorrect lastSyncedPush - was ${lastSyncedPush}, needs to be at maximum ${maxEntityChangeId}`);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.unrecoveredConsistencyErrors = true;
|
||||
|
||||
logFix(`Incorrect lastSyncedPush - is ${lastSyncedPush}, needs to be at maximum ${maxEntityChangeId}`);
|
||||
@@ -839,9 +888,9 @@ class ConsistencyChecks {
|
||||
return `${tableName}: ${count}`;
|
||||
}
|
||||
|
||||
const tables = [ "notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs" ];
|
||||
const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs"];
|
||||
|
||||
log.info(`Table counts: ${tables.map(tableName => getTableRowCount(tableName)).join(", ")}`);
|
||||
log.info(`Table counts: ${tables.map((tableName) => getTableRowCount(tableName)).join(", ")}`);
|
||||
}
|
||||
|
||||
async runChecks() {
|
||||
@@ -860,12 +909,9 @@ class ConsistencyChecks {
|
||||
if (this.unrecoveredConsistencyErrors) {
|
||||
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms)`);
|
||||
|
||||
ws.sendMessageToAllClients({type: 'consistency-checks-failed'});
|
||||
ws.sendMessageToAllClients({ type: "consistency-checks-failed" });
|
||||
} else {
|
||||
log.info(`All consistency checks passed ` +
|
||||
(this.fixedIssues ? "after some fixes" : "with no errors detected") +
|
||||
` (took ${elapsedTimeMs}ms)`
|
||||
);
|
||||
log.info(`All consistency checks passed ` + (this.fixedIssues ? "after some fixes" : "with no errors detected") + ` (took ${elapsedTimeMs}ms)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -875,11 +921,11 @@ function getBlankContent(isProtected: boolean, type: string, mime: string) {
|
||||
return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without a password
|
||||
}
|
||||
|
||||
if (mime === 'application/json') {
|
||||
return '{}';
|
||||
if (mime === "application/json") {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
return ''; // empty string might be a wrong choice for some note types, but it's the best guess
|
||||
return ""; // empty string might be a wrong choice for some note types, but it's the best guess
|
||||
}
|
||||
|
||||
function logFix(message: string) {
|
||||
@@ -891,7 +937,7 @@ function logError(message: string) {
|
||||
}
|
||||
|
||||
function runPeriodicChecks() {
|
||||
const autoFix = optionsService.getOptionBool('autoFixConsistencyIssues');
|
||||
const autoFix = optionsService.getOptionBool("autoFixConsistencyIssues");
|
||||
|
||||
const consistencyChecks = new ConsistencyChecks(autoFix);
|
||||
consistencyChecks.runChecks();
|
||||
|
||||
@@ -14,9 +14,9 @@ function getEntityHashes() {
|
||||
const startTime = new Date();
|
||||
|
||||
// we know this is slow and the total content hash calculation time is logged
|
||||
type HashRow = [ string, string, string, boolean ];
|
||||
const hashRows = sql.disableSlowQueryLogging(
|
||||
() => sql.getRawRows<HashRow>(`
|
||||
type HashRow = [string, string, string, boolean];
|
||||
const hashRows = sql.disableSlowQueryLogging(() =>
|
||||
sql.getRawRows<HashRow>(`
|
||||
SELECT entityName,
|
||||
entityId,
|
||||
hash,
|
||||
@@ -28,12 +28,12 @@ function getEntityHashes() {
|
||||
|
||||
// sorting is faster in memory
|
||||
// sorting by entityId is enough, hashes will be segmented by entityName later on anyway
|
||||
hashRows.sort((a, b) => a[1] < b[1] ? -1 : 1);
|
||||
hashRows.sort((a, b) => (a[1] < b[1] ? -1 : 1));
|
||||
|
||||
const hashMap: Record<string, SectorHash> = {};
|
||||
|
||||
for (const [entityName, entityId, hash, isErased] of hashRows) {
|
||||
const entityHashMap = hashMap[entityName] = hashMap[entityName] || {};
|
||||
const entityHashMap = (hashMap[entityName] = hashMap[entityName] || {});
|
||||
|
||||
const sector = entityId[0];
|
||||
|
||||
|
||||
@@ -15,13 +15,11 @@ import path from "path";
|
||||
function getAppDataDir() {
|
||||
let appDataDir = os.homedir(); // fallback if OS is not recognized
|
||||
|
||||
if (os.platform() === 'win32' && process.env.APPDATA) {
|
||||
if (os.platform() === "win32" && process.env.APPDATA) {
|
||||
appDataDir = process.env.APPDATA;
|
||||
}
|
||||
else if (os.platform() === 'linux') {
|
||||
} else if (os.platform() === "linux") {
|
||||
appDataDir = `${os.homedir()}/.local/share`;
|
||||
}
|
||||
else if (os.platform() === 'darwin') {
|
||||
} else if (os.platform() === "darwin") {
|
||||
appDataDir = `${os.homedir()}/Library/Application Support`;
|
||||
}
|
||||
|
||||
@@ -33,7 +31,7 @@ function getAppDataDir() {
|
||||
return appDataDir;
|
||||
}
|
||||
|
||||
const DIR_NAME = 'trilium-data';
|
||||
const DIR_NAME = "trilium-data";
|
||||
|
||||
function getTriliumDataDir() {
|
||||
if (process.env.TRILIUM_DATA_DIR) {
|
||||
|
||||
@@ -11,17 +11,26 @@ import hoistedNoteService from "./hoisted_note.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import { t } from "i18next";
|
||||
|
||||
const CALENDAR_ROOT_LABEL = 'calendarRoot';
|
||||
const YEAR_LABEL = 'yearNote';
|
||||
const MONTH_LABEL = 'monthNote';
|
||||
const DATE_LABEL = 'dateNote';
|
||||
const CALENDAR_ROOT_LABEL = "calendarRoot";
|
||||
const YEAR_LABEL = "yearNote";
|
||||
const MONTH_LABEL = "monthNote";
|
||||
const DATE_LABEL = "dateNote";
|
||||
|
||||
const WEEKDAY_TRANSLATION_IDS = [
|
||||
"weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"
|
||||
];
|
||||
const WEEKDAY_TRANSLATION_IDS = ["weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"];
|
||||
|
||||
const MONTH_TRANSLATION_IDS = [
|
||||
"months.january", "months.february", "months.march", "months.april", "months.may", "months.june", "months.july", "months.august", "months.september", "months.october", "months.november", "months.december"
|
||||
"months.january",
|
||||
"months.february",
|
||||
"months.march",
|
||||
"months.april",
|
||||
"months.may",
|
||||
"months.june",
|
||||
"months.july",
|
||||
"months.august",
|
||||
"months.september",
|
||||
"months.october",
|
||||
"months.november",
|
||||
"months.december"
|
||||
];
|
||||
|
||||
type StartOfWeek = "monday" | "sunday";
|
||||
@@ -30,9 +39,9 @@ function createNote(parentNote: BNote, noteTitle: string) {
|
||||
return noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title: noteTitle,
|
||||
content: '',
|
||||
content: "",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: 'text'
|
||||
type: "text"
|
||||
}).note;
|
||||
}
|
||||
|
||||
@@ -42,7 +51,7 @@ function getRootCalendarNote(): BNote {
|
||||
const workspaceNote = hoistedNoteService.getWorkspaceNote();
|
||||
|
||||
if (!workspaceNote || !workspaceNote.isRoot()) {
|
||||
rootNote = searchService.findFirstNoteWithQuery('#workspaceCalendarRoot', new SearchContext({ignoreHoistedNote: false}));
|
||||
rootNote = searchService.findFirstNoteWithQuery("#workspaceCalendarRoot", new SearchContext({ ignoreHoistedNote: false }));
|
||||
}
|
||||
|
||||
if (!rootNote) {
|
||||
@@ -52,16 +61,16 @@ function getRootCalendarNote(): BNote {
|
||||
if (!rootNote) {
|
||||
sql.transactional(() => {
|
||||
rootNote = noteService.createNewNote({
|
||||
parentNoteId: 'root',
|
||||
title: 'Calendar',
|
||||
target: 'into',
|
||||
parentNoteId: "root",
|
||||
title: "Calendar",
|
||||
target: "into",
|
||||
isProtected: false,
|
||||
type: 'text',
|
||||
content: ''
|
||||
type: "text",
|
||||
content: ""
|
||||
}).note;
|
||||
|
||||
attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
|
||||
attributeService.createLabel(rootNote.noteId, 'sorted');
|
||||
attributeService.createLabel(rootNote.noteId, "sorted");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,8 +82,7 @@ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
|
||||
const yearStr = dateStr.trim().substr(0, 4);
|
||||
|
||||
let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`,
|
||||
new SearchContext({ancestorNoteId: rootNote.noteId}));
|
||||
let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
||||
|
||||
if (yearNote) {
|
||||
return yearNote;
|
||||
@@ -84,12 +92,12 @@ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
yearNote = createNote(rootNote, yearStr);
|
||||
|
||||
attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
|
||||
attributeService.createLabel(yearNote.noteId, 'sorted');
|
||||
attributeService.createLabel(yearNote.noteId, "sorted");
|
||||
|
||||
const yearTemplateAttr = rootNote.getOwnedAttribute('relation', 'yearTemplate');
|
||||
const yearTemplateAttr = rootNote.getOwnedAttribute("relation", "yearTemplate");
|
||||
|
||||
if (yearTemplateAttr) {
|
||||
attributeService.createRelation(yearNote.noteId, 'template', yearTemplateAttr.value);
|
||||
attributeService.createRelation(yearNote.noteId, "template", yearTemplateAttr.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -101,8 +109,8 @@ function getMonthNoteTitle(rootNote: BNote, monthNumber: string, dateObj: Date)
|
||||
const monthName = t(MONTH_TRANSLATION_IDS[dateObj.getMonth()]);
|
||||
|
||||
return pattern
|
||||
.replace(/{shortMonth3}/g, monthName.slice(0,3))
|
||||
.replace(/{shortMonth4}/g, monthName.slice(0,4))
|
||||
.replace(/{shortMonth3}/g, monthName.slice(0, 3))
|
||||
.replace(/{shortMonth4}/g, monthName.slice(0, 4))
|
||||
.replace(/{monthNumberPadded}/g, monthNumber)
|
||||
.replace(/{month}/g, monthName);
|
||||
}
|
||||
@@ -113,8 +121,7 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
const monthStr = dateStr.substr(0, 7);
|
||||
const monthNumber = dateStr.substr(5, 2);
|
||||
|
||||
let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`,
|
||||
new SearchContext({ancestorNoteId: rootNote.noteId}));
|
||||
let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
||||
|
||||
if (monthNote) {
|
||||
return monthNote;
|
||||
@@ -130,12 +137,12 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
monthNote = createNote(yearNote, noteTitle);
|
||||
|
||||
attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
|
||||
attributeService.createLabel(monthNote.noteId, 'sorted');
|
||||
attributeService.createLabel(monthNote.noteId, "sorted");
|
||||
|
||||
const monthTemplateAttr = rootNote.getOwnedAttribute('relation', 'monthTemplate');
|
||||
const monthTemplateAttr = rootNote.getOwnedAttribute("relation", "monthTemplate");
|
||||
|
||||
if (monthTemplateAttr) {
|
||||
attributeService.createRelation(monthNote.noteId, 'template', monthTemplateAttr.value);
|
||||
attributeService.createRelation(monthNote.noteId, "template", monthTemplateAttr.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -168,8 +175,7 @@ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
|
||||
dateStr = dateStr.trim().substr(0, 10);
|
||||
|
||||
let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`,
|
||||
new SearchContext({ancestorNoteId: rootNote.noteId}));
|
||||
let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
||||
|
||||
if (dateNote) {
|
||||
return dateNote;
|
||||
@@ -187,10 +193,10 @@ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
|
||||
attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10));
|
||||
|
||||
const dateTemplateAttr = rootNote.getOwnedAttribute('relation', 'dateTemplate');
|
||||
const dateTemplateAttr = rootNote.getOwnedAttribute("relation", "dateTemplate");
|
||||
|
||||
if (dateTemplateAttr) {
|
||||
attributeService.createRelation(dateNote.noteId, 'template', dateTemplateAttr.value);
|
||||
attributeService.createRelation(dateNote.noteId, "template", dateTemplateAttr.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,13 +211,11 @@ function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) {
|
||||
const day = date.getDay();
|
||||
let diff;
|
||||
|
||||
if (startOfTheWeek === 'monday') {
|
||||
if (startOfTheWeek === "monday") {
|
||||
diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
|
||||
}
|
||||
else if (startOfTheWeek === 'sunday') {
|
||||
} else if (startOfTheWeek === "sunday") {
|
||||
diff = date.getDate() - day;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(`Unrecognized start of the week ${startOfTheWeek}`);
|
||||
}
|
||||
|
||||
@@ -219,7 +223,7 @@ function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) {
|
||||
}
|
||||
|
||||
interface WeekNoteOpts {
|
||||
startOfTheWeek?: StartOfWeek
|
||||
startOfTheWeek?: StartOfWeek;
|
||||
}
|
||||
|
||||
function getWeekNote(dateStr: string, options: WeekNoteOpts = {}, rootNote: BNote | null = null) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import dayjs from "dayjs";
|
||||
import cls from "./cls.js";
|
||||
|
||||
const LOCAL_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSSZZ';
|
||||
const UTC_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ssZ';
|
||||
const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ";
|
||||
const UTC_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ssZ";
|
||||
|
||||
function utcNowDateTime() {
|
||||
return utcDateTimeStr(new Date());
|
||||
@@ -12,8 +12,7 @@ function utcNowDateTime() {
|
||||
// so we'd prefer client timezone to be used to record local dates. For this reason, requests from clients contain
|
||||
// "trilium-local-now-datetime" header which is then stored in CLS
|
||||
function localNowDateTime() {
|
||||
return cls.getLocalNowDateTime()
|
||||
|| dayjs().format(LOCAL_DATETIME_FORMAT)
|
||||
return cls.getLocalNowDateTime() || dayjs().format(LOCAL_DATETIME_FORMAT);
|
||||
}
|
||||
|
||||
function localNowDate() {
|
||||
@@ -21,8 +20,7 @@ function localNowDate() {
|
||||
|
||||
if (clsDateTime) {
|
||||
return clsDateTime.substr(0, 10);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const date = new Date();
|
||||
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
@@ -34,11 +32,11 @@ function pad(num: number) {
|
||||
}
|
||||
|
||||
function utcDateStr(date: Date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function utcDateTimeStr(date: Date) {
|
||||
return date.toISOString().replace('T', ' ');
|
||||
return date.toISOString().replace("T", " ");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,8 +46,7 @@ function utcDateTimeStr(date: Date) {
|
||||
function parseDateTime(str: string) {
|
||||
try {
|
||||
return new Date(Date.parse(str));
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
throw new Error(`Can't parse date from '${str}': ${e.stack}`);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +59,7 @@ function parseLocalDate(str: string) {
|
||||
}
|
||||
|
||||
function getDateTimeForFile() {
|
||||
return new Date().toISOString().substr(0, 19).replace(/:/g, '');
|
||||
return new Date().toISOString().substr(0, 19).replace(/:/g, "");
|
||||
}
|
||||
|
||||
function validateLocalDateTime(str: string | null | undefined) {
|
||||
@@ -74,7 +71,6 @@ function validateLocalDateTime(str: string | null | undefined) {
|
||||
return `Invalid local date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110+0200'`;
|
||||
}
|
||||
|
||||
|
||||
if (!dayjs(str, LOCAL_DATETIME_FORMAT)) {
|
||||
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
|
||||
}
|
||||
@@ -89,7 +85,6 @@ function validateUtcDateTime(str: string | undefined) {
|
||||
return `Invalid UTC date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110Z'`;
|
||||
}
|
||||
|
||||
|
||||
if (!dayjs(str, UTC_DATETIME_FORMAT)) {
|
||||
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
|
||||
}
|
||||
|
||||
@@ -14,14 +14,13 @@ function arraysIdentical(a: any[] | Buffer, b: any[] | Buffer) {
|
||||
|
||||
function shaArray(content: crypto.BinaryLike) {
|
||||
// we use this as a simple checksum and don't rely on its security, so SHA-1 is good enough
|
||||
return crypto.createHash('sha1').update(content).digest();
|
||||
return crypto.createHash("sha1").update(content).digest();
|
||||
}
|
||||
|
||||
function pad(data: Buffer): Buffer {
|
||||
if (data.length > 16) {
|
||||
data = data.slice(0, 16);
|
||||
}
|
||||
else if (data.length < 16) {
|
||||
} else if (data.length < 16) {
|
||||
const zeros = Array(16 - data.length).fill(0);
|
||||
|
||||
data = Buffer.concat([data, Buffer.from(zeros)]);
|
||||
@@ -38,7 +37,7 @@ function encrypt(key: Buffer, plainText: Buffer | string) {
|
||||
const plainTextBuffer = Buffer.from(plainText);
|
||||
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', pad(key), pad(iv));
|
||||
const cipher = crypto.createCipheriv("aes-128-cbc", pad(key), pad(iv));
|
||||
|
||||
const digest = shaArray(plainTextBuffer).slice(0, 4);
|
||||
|
||||
@@ -48,7 +47,7 @@ function encrypt(key: Buffer, plainText: Buffer | string) {
|
||||
|
||||
const encryptedDataWithIv = Buffer.concat([iv, encryptedData]);
|
||||
|
||||
return encryptedDataWithIv.toString('base64');
|
||||
return encryptedDataWithIv.toString("base64");
|
||||
}
|
||||
|
||||
function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | null {
|
||||
@@ -61,7 +60,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
|
||||
}
|
||||
|
||||
try {
|
||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64');
|
||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
|
||||
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
|
||||
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
|
||||
@@ -70,7 +69,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
|
||||
|
||||
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv));
|
||||
const decipher = crypto.createDecipheriv("aes-128-cbc", pad(key), pad(iv));
|
||||
|
||||
const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]);
|
||||
|
||||
@@ -84,15 +83,13 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
// recovery from https://github.com/zadam/trilium/issues/510
|
||||
if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
|
||||
log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
|
||||
|
||||
return Buffer.from(cipherText);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +106,7 @@ function decryptString(dataKey: Buffer, cipherText: string) {
|
||||
throw new Error("Could not decrypt string.");
|
||||
}
|
||||
|
||||
return buffer.toString('utf-8');
|
||||
return buffer.toString("utf-8");
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -4,20 +4,19 @@ import optionService from "../options.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
function getVerificationHash(password: crypto.BinaryLike) {
|
||||
const salt = optionService.getOption('passwordVerificationSalt');
|
||||
const salt = optionService.getOption("passwordVerificationSalt");
|
||||
|
||||
return getScryptHash(password, salt);
|
||||
}
|
||||
|
||||
function getPasswordDerivedKey(password: crypto.BinaryLike) {
|
||||
const salt = optionService.getOption('passwordDerivedKeySalt');
|
||||
const salt = optionService.getOption("passwordDerivedKeySalt");
|
||||
|
||||
return getScryptHash(password, salt);
|
||||
}
|
||||
|
||||
function getScryptHash(password: crypto.BinaryLike, salt: crypto.BinaryLike) {
|
||||
const hashed = crypto.scryptSync(password, salt, 32,
|
||||
{N: 16384, r:8, p:1});
|
||||
const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
|
||||
|
||||
return hashed;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ function changePassword(currentPassword: string, newPassword: string) {
|
||||
sql.transactional(() => {
|
||||
const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword);
|
||||
|
||||
optionService.setOption('passwordVerificationSalt', randomSecureToken(32));
|
||||
optionService.setOption('passwordDerivedKeySalt', randomSecureToken(32));
|
||||
optionService.setOption("passwordVerificationSalt", randomSecureToken(32));
|
||||
optionService.setOption("passwordDerivedKeySalt", randomSecureToken(32));
|
||||
|
||||
const newPasswordVerificationKey = toBase64(myScryptService.getVerificationHash(newPassword));
|
||||
|
||||
@@ -35,7 +35,7 @@ function changePassword(currentPassword: string, newPassword: string) {
|
||||
passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
|
||||
}
|
||||
|
||||
optionService.setOption('passwordVerificationHash', newPasswordVerificationKey);
|
||||
optionService.setOption("passwordVerificationHash", newPasswordVerificationKey);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -48,14 +48,14 @@ function setPassword(password: string) {
|
||||
throw new Error("Password is set already. Either change it or perform 'reset password' first.");
|
||||
}
|
||||
|
||||
optionService.createOption('passwordVerificationSalt', randomSecureToken(32), true);
|
||||
optionService.createOption('passwordDerivedKeySalt', randomSecureToken(32), true);
|
||||
optionService.createOption("passwordVerificationSalt", randomSecureToken(32), true);
|
||||
optionService.createOption("passwordDerivedKeySalt", randomSecureToken(32), true);
|
||||
|
||||
const passwordVerificationKey = toBase64(myScryptService.getVerificationHash(password));
|
||||
optionService.createOption('passwordVerificationHash', passwordVerificationKey, true);
|
||||
optionService.createOption("passwordVerificationHash", passwordVerificationKey, true);
|
||||
|
||||
// passwordEncryptionService expects these options to already exist
|
||||
optionService.createOption('encryptedDataKey', '', true);
|
||||
optionService.createOption("encryptedDataKey", "", true);
|
||||
|
||||
passwordEncryptionService.setDataKey(password, randomSecureToken(16));
|
||||
|
||||
@@ -67,10 +67,10 @@ function setPassword(password: string) {
|
||||
function resetPassword() {
|
||||
// user forgot the password,
|
||||
sql.transactional(() => {
|
||||
optionService.setOption('passwordVerificationSalt', '');
|
||||
optionService.setOption('passwordDerivedKeySalt', '');
|
||||
optionService.setOption('encryptedDataKey', '');
|
||||
optionService.setOption('passwordVerificationHash', '');
|
||||
optionService.setOption("passwordVerificationSalt", "");
|
||||
optionService.setOption("passwordDerivedKeySalt", "");
|
||||
optionService.setOption("encryptedDataKey", "");
|
||||
optionService.setOption("passwordVerificationHash", "");
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@ import dataEncryptionService from "./data_encryption.js";
|
||||
function verifyPassword(password: string) {
|
||||
const givenPasswordHash = toBase64(myScryptService.getVerificationHash(password));
|
||||
|
||||
const dbPasswordHash = optionService.getOptionOrNull('passwordVerificationHash');
|
||||
const dbPasswordHash = optionService.getOptionOrNull("passwordVerificationHash");
|
||||
|
||||
if (!dbPasswordHash) {
|
||||
return false;
|
||||
@@ -20,13 +20,13 @@ function setDataKey(password: string, plainTextDataKey: string | Buffer) {
|
||||
|
||||
const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey);
|
||||
|
||||
optionService.setOption('encryptedDataKey', newEncryptedDataKey);
|
||||
optionService.setOption("encryptedDataKey", newEncryptedDataKey);
|
||||
}
|
||||
|
||||
function getDataKey(password: string) {
|
||||
const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
|
||||
|
||||
const encryptedDataKey = optionService.getOption('encryptedDataKey');
|
||||
const encryptedDataKey = optionService.getOption("encryptedDataKey");
|
||||
|
||||
const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey);
|
||||
|
||||
|
||||
@@ -6,26 +6,26 @@ import { randomString } from "./utils.js";
|
||||
import instanceId from "./instance_id.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import blobService from "../services/blob.js";
|
||||
import { EntityChange } from './entity_changes_interface.js';
|
||||
import { EntityChange } from "./entity_changes_interface.js";
|
||||
import type { Blob } from "./blob-interface.js";
|
||||
import eventService from "./events.js";
|
||||
|
||||
let maxEntityChangeId = 0;
|
||||
|
||||
function putEntityChangeWithInstanceId(origEntityChange: EntityChange, instanceId: string) {
|
||||
const ec = {...origEntityChange, instanceId};
|
||||
const ec = { ...origEntityChange, instanceId };
|
||||
|
||||
putEntityChange(ec);
|
||||
}
|
||||
|
||||
function putEntityChangeWithForcedChange(origEntityChange: EntityChange) {
|
||||
const ec = {...origEntityChange, changeId: null};
|
||||
const ec = { ...origEntityChange, changeId: null };
|
||||
|
||||
putEntityChange(ec);
|
||||
}
|
||||
|
||||
function putEntityChange(origEntityChange: EntityChange) {
|
||||
const ec = {...origEntityChange};
|
||||
const ec = { ...origEntityChange };
|
||||
|
||||
delete ec.id;
|
||||
|
||||
@@ -50,7 +50,7 @@ function putNoteReorderingEntityChange(parentNoteId: string, componentId?: strin
|
||||
putEntityChange({
|
||||
entityName: "note_reordering",
|
||||
entityId: parentNoteId,
|
||||
hash: 'N/A',
|
||||
hash: "N/A",
|
||||
isErased: false,
|
||||
utcDateChanged: dateUtils.utcNowDateTime(),
|
||||
isSynced: true,
|
||||
@@ -59,7 +59,7 @@ function putNoteReorderingEntityChange(parentNoteId: string, componentId?: strin
|
||||
});
|
||||
|
||||
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
entityName: 'note_reordering',
|
||||
entityName: "note_reordering",
|
||||
entity: sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId])
|
||||
});
|
||||
}
|
||||
@@ -78,10 +78,10 @@ function addEntityChangesForSector(entityName: string, sector: string) {
|
||||
let entitiesInserted = entityChanges.length;
|
||||
|
||||
sql.transactional(() => {
|
||||
if (entityName === 'blobs') {
|
||||
entitiesInserted += addEntityChangesForDependingEntity(sector, 'notes', 'noteId');
|
||||
entitiesInserted += addEntityChangesForDependingEntity(sector, 'attachments', 'attachmentId');
|
||||
entitiesInserted += addEntityChangesForDependingEntity(sector, 'revisions', 'revisionId');
|
||||
if (entityName === "blobs") {
|
||||
entitiesInserted += addEntityChangesForDependingEntity(sector, "notes", "noteId");
|
||||
entitiesInserted += addEntityChangesForDependingEntity(sector, "attachments", "attachmentId");
|
||||
entitiesInserted += addEntityChangesForDependingEntity(sector, "revisions", "revisionId");
|
||||
}
|
||||
|
||||
for (const ec of entityChanges) {
|
||||
@@ -94,12 +94,15 @@ function addEntityChangesForSector(entityName: string, sector: string) {
|
||||
|
||||
function addEntityChangesForDependingEntity(sector: string, tableName: string, primaryKeyColumn: string) {
|
||||
// problem in blobs might be caused by problem in entity referencing the blob
|
||||
const dependingEntityChanges = sql.getRows<EntityChange>(`
|
||||
const dependingEntityChanges = sql.getRows<EntityChange>(
|
||||
`
|
||||
SELECT dep_change.*
|
||||
FROM entity_changes orig_sector
|
||||
JOIN ${tableName} ON ${tableName}.blobId = orig_sector.entityId
|
||||
JOIN entity_changes dep_change ON dep_change.entityName = '${tableName}' AND dep_change.entityId = ${tableName}.${primaryKeyColumn}
|
||||
WHERE orig_sector.entityName = 'blobs' AND SUBSTR(orig_sector.entityId, 1, 1) = ?`, [sector]);
|
||||
WHERE orig_sector.entityName = 'blobs' AND SUBSTR(orig_sector.entityId, 1, 1) = ?`,
|
||||
[sector]
|
||||
);
|
||||
|
||||
for (const ec of dependingEntityChanges) {
|
||||
putEntityChangeWithForcedChange(ec);
|
||||
@@ -118,7 +121,7 @@ function cleanupEntityChangesForMissingEntities(entityName: string, entityPrimar
|
||||
AND entityId NOT IN (SELECT ${entityPrimaryKey} FROM ${entityName})`);
|
||||
}
|
||||
|
||||
function fillEntityChanges(entityName: string, entityPrimaryKey: string, condition = '') {
|
||||
function fillEntityChanges(entityName: string, entityPrimaryKey: string, condition = "") {
|
||||
cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey);
|
||||
|
||||
sql.transactional(() => {
|
||||
@@ -142,7 +145,7 @@ function fillEntityChanges(entityName: string, entityPrimaryKey: string, conditi
|
||||
isErased: false
|
||||
};
|
||||
|
||||
if (entityName === 'blobs') {
|
||||
if (entityName === "blobs") {
|
||||
const blob = sql.getRow<Blob>("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]);
|
||||
ec.hash = blobService.calculateContentHash(blob);
|
||||
ec.utcDateChanged = blob.utcDateModified;
|
||||
@@ -153,7 +156,7 @@ function fillEntityChanges(entityName: string, entityPrimaryKey: string, conditi
|
||||
if (entity) {
|
||||
ec.hash = entity.generateHash();
|
||||
ec.utcDateChanged = entity.getUtcDateChanged() || dateUtils.utcNowDateTime();
|
||||
ec.isSynced = entityName !== 'options' || !!entity.isSynced;
|
||||
ec.isSynced = entityName !== "options" || !!entity.isSynced;
|
||||
} else {
|
||||
// entity might be null (not present in becca) when it's deleted
|
||||
// this will produce different hash value than when entity is being deleted since then
|
||||
@@ -184,7 +187,7 @@ function fillAllEntityChanges() {
|
||||
fillEntityChanges("blobs", "blobId");
|
||||
fillEntityChanges("attributes", "attributeId");
|
||||
fillEntityChanges("etapi_tokens", "etapiTokenId");
|
||||
fillEntityChanges("options", "name", 'WHERE isSynced = 1');
|
||||
fillEntityChanges("options", "name", "WHERE isSynced = 1");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function isDev() {
|
||||
return !!(process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === 'dev');
|
||||
return !!(process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === "dev");
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,18 +16,15 @@ function eraseNotes(noteIdsToErase: string[]) {
|
||||
setEntityChangesAsErased(sql.getManyRows(`SELECT * FROM entity_changes WHERE entityName = 'notes' AND entityId IN (???)`, noteIdsToErase));
|
||||
|
||||
// we also need to erase all "dependent" entities of the erased notes
|
||||
const branchIdsToErase = sql.getManyRows<{ branchId: string }>(`SELECT branchId FROM branches WHERE noteId IN (???)`, noteIdsToErase)
|
||||
.map(row => row.branchId);
|
||||
const branchIdsToErase = sql.getManyRows<{ branchId: string }>(`SELECT branchId FROM branches WHERE noteId IN (???)`, noteIdsToErase).map((row) => row.branchId);
|
||||
|
||||
eraseBranches(branchIdsToErase);
|
||||
|
||||
const attributeIdsToErase = sql.getManyRows<{ attributeId: string }>(`SELECT attributeId FROM attributes WHERE noteId IN (???)`, noteIdsToErase)
|
||||
.map(row => row.attributeId);
|
||||
const attributeIdsToErase = sql.getManyRows<{ attributeId: string }>(`SELECT attributeId FROM attributes WHERE noteId IN (???)`, noteIdsToErase).map((row) => row.attributeId);
|
||||
|
||||
eraseAttributes(attributeIdsToErase);
|
||||
|
||||
const revisionIdsToErase = sql.getManyRows<{ revisionId: string }>(`SELECT revisionId FROM revisions WHERE noteId IN (???)`, noteIdsToErase)
|
||||
.map(row => row.revisionId);
|
||||
const revisionIdsToErase = sql.getManyRows<{ revisionId: string }>(`SELECT revisionId FROM revisions WHERE noteId IN (???)`, noteIdsToErase).map((row) => row.revisionId);
|
||||
|
||||
eraseRevisions(revisionIdsToErase);
|
||||
|
||||
@@ -120,7 +117,7 @@ function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds: number | null = n
|
||||
// this is important also so that the erased entity changes are sent to the connected clients
|
||||
sql.transactional(() => {
|
||||
if (eraseEntitiesAfterTimeInSeconds === null) {
|
||||
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds');
|
||||
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt("eraseEntitiesAfterTimeInSeconds");
|
||||
}
|
||||
|
||||
const cutoffDate = new Date(Date.now() - eraseEntitiesAfterTimeInSeconds * 1000);
|
||||
@@ -167,11 +164,11 @@ function eraseUnusedAttachmentsNow() {
|
||||
|
||||
function eraseScheduledAttachments(eraseUnusedAttachmentsAfterSeconds: number | null = null) {
|
||||
if (eraseUnusedAttachmentsAfterSeconds === null) {
|
||||
eraseUnusedAttachmentsAfterSeconds = optionService.getOptionInt('eraseUnusedAttachmentsAfterSeconds');
|
||||
eraseUnusedAttachmentsAfterSeconds = optionService.getOptionInt("eraseUnusedAttachmentsAfterSeconds");
|
||||
}
|
||||
|
||||
const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - (eraseUnusedAttachmentsAfterSeconds * 1000)));
|
||||
const attachmentIdsToErase = sql.getColumn<string>('SELECT attachmentId FROM attachments WHERE utcDateScheduledForErasureSince < ?', [cutOffDate]);
|
||||
const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - eraseUnusedAttachmentsAfterSeconds * 1000));
|
||||
const attachmentIdsToErase = sql.getColumn<string>("SELECT attachmentId FROM attachments WHERE utcDateScheduledForErasureSince < ?", [cutOffDate]);
|
||||
|
||||
eraseAttachments(attachmentIdsToErase);
|
||||
}
|
||||
@@ -179,11 +176,23 @@ function eraseScheduledAttachments(eraseUnusedAttachmentsAfterSeconds: number |
|
||||
export function startScheduledCleanup() {
|
||||
sqlInit.dbReady.then(() => {
|
||||
// first cleanup kickoff 5 minutes after startup
|
||||
setTimeout(cls.wrap(() => eraseDeletedEntities()), 5 * 60 * 1000);
|
||||
setTimeout(cls.wrap(() => eraseScheduledAttachments()), 6 * 60 * 1000);
|
||||
setTimeout(
|
||||
cls.wrap(() => eraseDeletedEntities()),
|
||||
5 * 60 * 1000
|
||||
);
|
||||
setTimeout(
|
||||
cls.wrap(() => eraseScheduledAttachments()),
|
||||
6 * 60 * 1000
|
||||
);
|
||||
|
||||
setInterval(cls.wrap(() => eraseDeletedEntities()), 4 * 3600 * 1000);
|
||||
setInterval(cls.wrap(() => eraseScheduledAttachments()), 3600 * 1000);
|
||||
setInterval(
|
||||
cls.wrap(() => eraseDeletedEntities()),
|
||||
4 * 3600 * 1000
|
||||
);
|
||||
setInterval(
|
||||
cls.wrap(() => eraseScheduledAttachments()),
|
||||
3600 * 1000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,5 +202,5 @@ export default {
|
||||
eraseNotesWithDeleteId,
|
||||
eraseUnusedBlobs,
|
||||
eraseAttachments,
|
||||
eraseRevisions,
|
||||
eraseRevisions
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ function getTokens() {
|
||||
}
|
||||
|
||||
function getTokenHash(token: crypto.BinaryLike) {
|
||||
return crypto.createHash('sha256').update(token).digest('base64');
|
||||
return crypto.createHash("sha256").update(token).digest("base64");
|
||||
}
|
||||
|
||||
function createToken(tokenName: string) {
|
||||
@@ -52,14 +52,12 @@ function parseAuthToken(auth: string | undefined) {
|
||||
|
||||
if (chunks.length === 1) {
|
||||
return { token: auth }; // legacy format without etapiTokenId
|
||||
}
|
||||
else if (chunks.length === 2) {
|
||||
} else if (chunks.length === 2) {
|
||||
return {
|
||||
etapiTokenId: chunks[0],
|
||||
token: chunks[1]
|
||||
}
|
||||
}
|
||||
else {
|
||||
};
|
||||
} else {
|
||||
return null; // wrong format
|
||||
}
|
||||
}
|
||||
@@ -81,8 +79,7 @@ function isValidAuthHeader(auth: string | undefined) {
|
||||
}
|
||||
|
||||
return etapiToken.tokenHash === authTokenHash;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
for (const etapiToken of becca.getEtapiTokens()) {
|
||||
if (etapiToken.tokenHash === authTokenHash) {
|
||||
return true;
|
||||
|
||||
@@ -21,7 +21,7 @@ const eventListeners: Record<string, EventListener[]> = {};
|
||||
*/
|
||||
function subscribe(eventTypes: EventType, listener: EventListener) {
|
||||
if (!Array.isArray(eventTypes)) {
|
||||
eventTypes = [ eventTypes ];
|
||||
eventTypes = [eventTypes];
|
||||
}
|
||||
|
||||
for (const eventType of eventTypes) {
|
||||
@@ -32,7 +32,7 @@ function subscribe(eventTypes: EventType, listener: EventListener) {
|
||||
|
||||
function subscribeBeccaLoader(eventTypes: EventType, listener: EventListener) {
|
||||
if (!Array.isArray(eventTypes)) {
|
||||
eventTypes = [ eventTypes ];
|
||||
eventTypes = [eventTypes];
|
||||
}
|
||||
|
||||
for (const eventType of eventTypes) {
|
||||
@@ -50,8 +50,7 @@ function emit(eventType: string, data?: any) {
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(data);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
log.error(`Listener threw error: ${e.message}, stack: ${e.stack}`);
|
||||
// we won't stop execution because of listener
|
||||
}
|
||||
|
||||
@@ -6,36 +6,27 @@ import turndownPluginGfm from "joplin-turndown-plugin-gfm";
|
||||
let instance: TurndownService | null = null;
|
||||
|
||||
const fencedCodeBlockFilter: TurndownService.Rule = {
|
||||
filter: function (node, options) {
|
||||
return (
|
||||
options.codeBlockStyle === 'fenced' &&
|
||||
node.nodeName === 'PRE' &&
|
||||
node.firstChild !== null &&
|
||||
node.firstChild.nodeName === 'CODE'
|
||||
)
|
||||
},
|
||||
filter: function (node, options) {
|
||||
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
|
||||
},
|
||||
|
||||
replacement: function (content, node, options) {
|
||||
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
|
||||
return content;
|
||||
replacement: function (content, node, options) {
|
||||
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
|
||||
return content;
|
||||
}
|
||||
|
||||
const className = node.firstChild.getAttribute("class") || "";
|
||||
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
|
||||
|
||||
return "\n\n" + options.fence + language + "\n" + node.firstChild.textContent + "\n" + options.fence + "\n\n";
|
||||
}
|
||||
|
||||
const className = node.firstChild.getAttribute('class') || ''
|
||||
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ''])[1]);
|
||||
|
||||
return (
|
||||
'\n\n' + options.fence + language + '\n' +
|
||||
node.firstChild.textContent +
|
||||
'\n' + options.fence + '\n\n'
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
function toMarkdown(content: string) {
|
||||
if (instance === null) {
|
||||
instance = new TurndownService({ codeBlockStyle: 'fenced' });
|
||||
instance = new TurndownService({ codeBlockStyle: "fenced" });
|
||||
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
|
||||
instance.addRule('fencedCodeBlock', fencedCodeBlockFilter);
|
||||
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
|
||||
instance.use(turndownPluginGfm.gfm);
|
||||
}
|
||||
|
||||
@@ -44,16 +35,14 @@ function toMarkdown(content: string) {
|
||||
|
||||
function rewriteLanguageTag(source: string) {
|
||||
if (!source) {
|
||||
return source;
|
||||
return source;
|
||||
}
|
||||
|
||||
if (source === "text-x-trilium-auto") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return source
|
||||
.split("-")
|
||||
.at(-1);
|
||||
return source.split("-").at(-1);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { getContentDisposition, stripTags } from "../utils.js";
|
||||
import becca from "../../becca/becca.js";
|
||||
import TaskContext from "../task_context.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
import { Response } from 'express';
|
||||
import { Response } from "express";
|
||||
|
||||
function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string, res: Response) {
|
||||
if (!['1.0', '2.0'].includes(version)) {
|
||||
if (!["1.0", "2.0"].includes(version)) {
|
||||
throw new Error(`Unrecognized OPML version ${version}`);
|
||||
}
|
||||
|
||||
@@ -17,30 +17,32 @@ function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string
|
||||
|
||||
function exportNoteInner(branchId: string) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
if (!branch) { throw new Error("Unable to find branch."); }
|
||||
if (!branch) {
|
||||
throw new Error("Unable to find branch.");
|
||||
}
|
||||
|
||||
const note = branch.getNote();
|
||||
if (!note) { throw new Error("Unable to find note."); }
|
||||
if (!note) {
|
||||
throw new Error("Unable to find note.");
|
||||
}
|
||||
|
||||
if (note.hasOwnedLabel('excludeFromExport')) {
|
||||
if (note.hasOwnedLabel("excludeFromExport")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = `${branch.prefix ? (`${branch.prefix} - `) : ''}${note.title}`;
|
||||
const title = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.title}`;
|
||||
|
||||
if (opmlVersion === 1) {
|
||||
const preparedTitle = escapeXmlAttribute(title);
|
||||
const preparedContent = note.hasStringContent() ? prepareText(note.getContent() as string) : '';
|
||||
const preparedContent = note.hasStringContent() ? prepareText(note.getContent() as string) : "";
|
||||
|
||||
res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
|
||||
}
|
||||
else if (opmlVersion === 2) {
|
||||
} else if (opmlVersion === 2) {
|
||||
const preparedTitle = escapeXmlAttribute(title);
|
||||
const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent() as string) : '';
|
||||
const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent() as string) : "";
|
||||
|
||||
res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(`Unrecognized OPML version ${opmlVersion}`);
|
||||
}
|
||||
|
||||
@@ -52,14 +54,13 @@ function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string
|
||||
}
|
||||
}
|
||||
|
||||
res.write('</outline>');
|
||||
res.write("</outline>");
|
||||
}
|
||||
|
||||
const filename = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.title}.opml`;
|
||||
|
||||
const filename = `${branch.prefix ? (`${branch.prefix} - `) : ''}${note.title}.opml`;
|
||||
|
||||
res.setHeader('Content-Disposition', getContentDisposition(filename));
|
||||
res.setHeader('Content-Type', 'text/x-opml');
|
||||
res.setHeader("Content-Disposition", getContentDisposition(filename));
|
||||
res.setHeader("Content-Type", "text/x-opml");
|
||||
|
||||
res.write(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="${version}">
|
||||
@@ -80,22 +81,17 @@ function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string
|
||||
}
|
||||
|
||||
function prepareText(text: string) {
|
||||
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n')
|
||||
.replace(/ /g, ' '); // nbsp isn't in XML standard (only HTML)
|
||||
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, "\n").replace(/ /g, " "); // nbsp isn't in XML standard (only HTML)
|
||||
|
||||
const stripped = stripTags(newLines);
|
||||
|
||||
const escaped = escapeXmlAttribute(stripped);
|
||||
|
||||
return escaped.replace(/\n/g, ' ');
|
||||
return escaped.replace(/\n/g, " ");
|
||||
}
|
||||
|
||||
function escapeXmlAttribute(text: string) {
|
||||
return text.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -7,16 +7,16 @@ import mdService from "./md.js";
|
||||
import becca from "../../becca/becca.js";
|
||||
import TaskContext from "../task_context.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
import { Response } from 'express';
|
||||
import { Response } from "express";
|
||||
|
||||
function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response) {
|
||||
const note = branch.getNote();
|
||||
|
||||
if (note.type === 'image' || note.type === 'file') {
|
||||
if (note.type === "image" || note.type === "file") {
|
||||
return [400, `Note type '${note.type}' cannot be exported as single file.`];
|
||||
}
|
||||
|
||||
if (format !== 'html' && format !== 'markdown') {
|
||||
if (format !== "html" && format !== "markdown") {
|
||||
return [400, `Unrecognized format '${format}'`];
|
||||
}
|
||||
|
||||
@@ -27,42 +27,37 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht
|
||||
throw new Error("Unsupported content type for export.");
|
||||
}
|
||||
|
||||
if (note.type === 'text') {
|
||||
if (format === 'html') {
|
||||
if (note.type === "text") {
|
||||
if (format === "html") {
|
||||
content = inlineAttachments(content);
|
||||
|
||||
if (!content.toLowerCase().includes("<html")) {
|
||||
content = `<html><head><meta charset="utf-8"></head><body>${content}</body></html>`;
|
||||
}
|
||||
|
||||
payload = content.length < 100_000
|
||||
? html.prettyPrint(content, {indent_size: 2})
|
||||
: content;
|
||||
payload = content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content;
|
||||
|
||||
extension = 'html';
|
||||
mime = 'text/html';
|
||||
}
|
||||
else if (format === 'markdown') {
|
||||
extension = "html";
|
||||
mime = "text/html";
|
||||
} else if (format === "markdown") {
|
||||
payload = mdService.toMarkdown(content);
|
||||
extension = 'md';
|
||||
mime = 'text/x-markdown'
|
||||
extension = "md";
|
||||
mime = "text/x-markdown";
|
||||
}
|
||||
}
|
||||
else if (note.type === 'code') {
|
||||
} else if (note.type === "code") {
|
||||
payload = content;
|
||||
extension = mimeTypes.extension(note.mime) || 'code';
|
||||
extension = mimeTypes.extension(note.mime) || "code";
|
||||
mime = note.mime;
|
||||
}
|
||||
else if (note.type === 'relationMap' || note.type === 'canvas' || note.type === 'search') {
|
||||
} else if (note.type === "relationMap" || note.type === "canvas" || note.type === "search") {
|
||||
payload = content;
|
||||
extension = 'json';
|
||||
mime = 'application/json';
|
||||
extension = "json";
|
||||
mime = "application/json";
|
||||
}
|
||||
|
||||
const fileName = `${note.title}.${extension}`;
|
||||
|
||||
res.setHeader('Content-Disposition', getContentDisposition(fileName));
|
||||
res.setHeader('Content-Type', `${mime}; charset=UTF-8`);
|
||||
res.setHeader("Content-Disposition", getContentDisposition(fileName));
|
||||
res.setHeader("Content-Type", `${mime}; charset=UTF-8`);
|
||||
|
||||
res.send(payload);
|
||||
|
||||
@@ -73,7 +68,7 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht
|
||||
function inlineAttachments(content: string) {
|
||||
content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/?[^"]+"/g, (match, noteId) => {
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note || !note.mime.startsWith('image/')) {
|
||||
if (!note || !note.mime.startsWith("image/")) {
|
||||
return match;
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ function inlineAttachments(content: string) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const base64Content = imageContent.toString('base64');
|
||||
const base64Content = imageContent.toString("base64");
|
||||
const srcValue = `data:${note.mime};base64,${base64Content}`;
|
||||
|
||||
return `src="${srcValue}"`;
|
||||
@@ -90,7 +85,7 @@ function inlineAttachments(content: string) {
|
||||
|
||||
content = content.replace(/src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image\/?[^"]+"/g, (match, attachmentId) => {
|
||||
const attachment = becca.getAttachment(attachmentId);
|
||||
if (!attachment || !attachment.mime.startsWith('image/')) {
|
||||
if (!attachment || !attachment.mime.startsWith("image/")) {
|
||||
return match;
|
||||
}
|
||||
|
||||
@@ -99,7 +94,7 @@ function inlineAttachments(content: string) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const base64Content = attachmentContent.toString('base64');
|
||||
const base64Content = attachmentContent.toString("base64");
|
||||
const srcValue = `data:${attachment.mime};base64,${base64Content}`;
|
||||
|
||||
return `src="${srcValue}"`;
|
||||
@@ -116,7 +111,7 @@ function inlineAttachments(content: string) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const base64Content = attachmentContent.toString('base64');
|
||||
const base64Content = attachmentContent.toString("base64");
|
||||
const hrefValue = `data:${attachment.mime};base64,${base64Content}`;
|
||||
|
||||
return `href="${hrefValue}" download="${escapeHtml(attachment.title)}"`;
|
||||
|
||||
@@ -19,15 +19,15 @@ import NoteMeta from "../meta/note_meta.js";
|
||||
import AttachmentMeta from "../meta/attachment_meta.js";
|
||||
import AttributeMeta from "../meta/attribute_meta.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
import { Response } from 'express';
|
||||
import { Response } from "express";
|
||||
import { RESOURCE_DIR } from "../resource_dir.js";
|
||||
|
||||
async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true) {
|
||||
if (!['html', 'markdown'].includes(format)) {
|
||||
if (!["html", "markdown"].includes(format)) {
|
||||
throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`);
|
||||
}
|
||||
|
||||
const archive = archiver('zip', {
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 9 } // Sets the compression level.
|
||||
});
|
||||
|
||||
@@ -44,12 +44,10 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
index = existingFileNames[lcFileName]++;
|
||||
|
||||
newName = `${index}_${lcFileName}`;
|
||||
}
|
||||
while (newName in existingFileNames);
|
||||
} while (newName in existingFileNames);
|
||||
|
||||
return `${index}_${fileName}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
existingFileNames[lcFileName] = 1;
|
||||
|
||||
return fileName;
|
||||
@@ -63,7 +61,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
if (fileName.length > 30) {
|
||||
// We use regex to match the extension to preserve multiple dots in extensions (e.g. .tar.gz).
|
||||
let match = fileName.match(/(\.[a-zA-Z0-9_.!#-]+)$/);
|
||||
let ext = match ? match[0] : '';
|
||||
let ext = match ? match[0] : "";
|
||||
// Crop the extension if extension length exceeds 30
|
||||
const croppedExt = ext.slice(-30);
|
||||
// Crop the file name section and append the cropped extension
|
||||
@@ -75,26 +73,22 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
|
||||
// the following two are handled specifically since we always want to have these extensions no matter the automatic detection
|
||||
// and/or existing detected extensions in the note name
|
||||
if (type === 'text' && format === 'markdown') {
|
||||
newExtension = 'md';
|
||||
}
|
||||
else if (type === 'text' && format === 'html') {
|
||||
newExtension = 'html';
|
||||
}
|
||||
else if (mime === 'application/x-javascript' || mime === 'text/javascript') {
|
||||
newExtension = 'js';
|
||||
}
|
||||
else if (type === 'canvas' || mime === 'application/json') {
|
||||
newExtension = 'json';
|
||||
}
|
||||
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
|
||||
if (type === "text" && format === "markdown") {
|
||||
newExtension = "md";
|
||||
} else if (type === "text" && format === "html") {
|
||||
newExtension = "html";
|
||||
} else if (mime === "application/x-javascript" || mime === "text/javascript") {
|
||||
newExtension = "js";
|
||||
} else if (type === "canvas" || mime === "application/json") {
|
||||
newExtension = "json";
|
||||
} else if (existingExtension.length > 0) {
|
||||
// if the page already has an extension, then we'll just keep it
|
||||
newExtension = null;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (mime?.toLowerCase()?.trim() === "image/jpg") {
|
||||
newExtension = 'jpg';
|
||||
newExtension = "jpg";
|
||||
} else if (mime?.toLowerCase()?.trim() === "text/mermaid") {
|
||||
newExtension = 'txt';
|
||||
newExtension = "txt";
|
||||
} else {
|
||||
newExtension = mimeTypes.extension(mime) || "dat";
|
||||
}
|
||||
@@ -111,23 +105,26 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
function createNoteMeta(branch: BBranch, parentMeta: Partial<NoteMeta>, existingFileNames: Record<string, number>): NoteMeta | null {
|
||||
const note = branch.getNote();
|
||||
|
||||
if (note.hasOwnedLabel('excludeFromExport')) {
|
||||
if (note.hasOwnedLabel("excludeFromExport")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = note.getTitleOrProtected();
|
||||
const completeTitle = branch.prefix ? (`${branch.prefix} - ${title}`) : title;
|
||||
const completeTitle = branch.prefix ? `${branch.prefix} - ${title}` : title;
|
||||
let baseFileName = sanitize(completeTitle);
|
||||
|
||||
if (baseFileName.length > 200) { // the actual limit is 256 bytes(!) but let's be conservative
|
||||
if (baseFileName.length > 200) {
|
||||
// the actual limit is 256 bytes(!) but let's be conservative
|
||||
baseFileName = baseFileName.substr(0, 200);
|
||||
}
|
||||
|
||||
if (!parentMeta.notePath) { throw new Error("Missing parent note path."); }
|
||||
if (!parentMeta.notePath) {
|
||||
throw new Error("Missing parent note path.");
|
||||
}
|
||||
const notePath = parentMeta.notePath.concat([note.noteId]);
|
||||
|
||||
if (note.noteId in noteIdToMeta) {
|
||||
const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === 'html' ? 'html' : 'md'}`);
|
||||
const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === "html" ? "html" : "md"}`);
|
||||
|
||||
const meta: NoteMeta = {
|
||||
isClone: true,
|
||||
@@ -136,7 +133,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
title: note.getTitleOrProtected(),
|
||||
prefix: branch.prefix,
|
||||
dataFileName: fileName,
|
||||
type: 'text', // export will have text description
|
||||
type: "text", // export will have text description
|
||||
format: format
|
||||
};
|
||||
return meta;
|
||||
@@ -152,7 +149,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
meta.isExpanded = branch.isExpanded;
|
||||
meta.type = note.type;
|
||||
meta.mime = note.mime;
|
||||
meta.attributes = note.getOwnedAttributes().map(attribute => {
|
||||
meta.attributes = note.getOwnedAttributes().map((attribute) => {
|
||||
const attrMeta: AttributeMeta = {
|
||||
type: attribute.type,
|
||||
name: attribute.name,
|
||||
@@ -166,7 +163,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
if (note.type === 'text') {
|
||||
if (note.type === "text") {
|
||||
meta.format = format;
|
||||
}
|
||||
|
||||
@@ -174,8 +171,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
|
||||
// sort children for having a stable / reproducible export format
|
||||
note.sortChildren();
|
||||
const childBranches = note.getChildBranches()
|
||||
.filter(branch => branch?.noteId !== '_hidden');
|
||||
const childBranches = note.getChildBranches().filter((branch) => branch?.noteId !== "_hidden");
|
||||
|
||||
const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
|
||||
|
||||
@@ -185,23 +181,17 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
}
|
||||
|
||||
const attachments = note.getAttachments();
|
||||
meta.attachments = attachments
|
||||
.map(attachment => {
|
||||
const attMeta: AttachmentMeta = {
|
||||
attachmentId: attachment.attachmentId,
|
||||
title: attachment.title,
|
||||
role: attachment.role,
|
||||
mime: attachment.mime,
|
||||
position: attachment.position,
|
||||
dataFileName: getDataFileName(
|
||||
null,
|
||||
attachment.mime,
|
||||
baseFileName + "_" + attachment.title,
|
||||
existingFileNames
|
||||
)
|
||||
};
|
||||
return attMeta;
|
||||
});
|
||||
meta.attachments = attachments.map((attachment) => {
|
||||
const attMeta: AttachmentMeta = {
|
||||
attachmentId: attachment.attachmentId,
|
||||
title: attachment.title,
|
||||
role: attachment.role,
|
||||
mime: attachment.mime,
|
||||
position: attachment.position,
|
||||
dataFileName: getDataFileName(null, attachment.mime, baseFileName + "_" + attachment.title, existingFileNames)
|
||||
};
|
||||
return attMeta;
|
||||
});
|
||||
|
||||
if (childBranches.length > 0) {
|
||||
meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName);
|
||||
@@ -211,7 +201,9 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
const childExistingNames = {};
|
||||
|
||||
for (const childBranch of childBranches) {
|
||||
if (!childBranch) { continue; }
|
||||
if (!childBranch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const note = createNoteMeta(childBranch, meta as NoteMeta, childExistingNames);
|
||||
|
||||
@@ -288,7 +280,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
function findAttachment(targetAttachmentId: string) {
|
||||
let url;
|
||||
|
||||
const attachmentMeta = (noteMeta.attachments || []).find(attMeta => attMeta.attachmentId === targetAttachmentId);
|
||||
const attachmentMeta = (noteMeta.attachments || []).find((attMeta) => attMeta.attachmentId === targetAttachmentId);
|
||||
if (attachmentMeta) {
|
||||
// easy job here, because attachment will be in the same directory as the note's data file.
|
||||
url = attachmentMeta.dataFileName;
|
||||
@@ -300,15 +292,17 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
}
|
||||
|
||||
function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
|
||||
if (['html', 'markdown'].includes(noteMeta?.format || "")) {
|
||||
if (["html", "markdown"].includes(noteMeta?.format || "")) {
|
||||
content = content.toString();
|
||||
|
||||
content = rewriteLinks(content, noteMeta);
|
||||
}
|
||||
|
||||
if (noteMeta.format === 'html' && typeof content === "string") {
|
||||
if (noteMeta.format === "html" && typeof content === "string") {
|
||||
if (!content.substr(0, 100).toLowerCase().includes("<html")) {
|
||||
if (!noteMeta?.notePath?.length) { throw new Error("Missing note path."); }
|
||||
if (!noteMeta?.notePath?.length) {
|
||||
throw new Error("Missing note path.");
|
||||
}
|
||||
|
||||
const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`;
|
||||
const htmlTitle = escapeHtml(title);
|
||||
@@ -332,10 +326,8 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
</html>`;
|
||||
}
|
||||
|
||||
return content.length < 100_000
|
||||
? html.prettyPrint(content, {indent_size: 2})
|
||||
: content;
|
||||
} else if (noteMeta.format === 'markdown' && typeof content === "string") {
|
||||
return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content;
|
||||
} else if (noteMeta.format === "markdown" && typeof content === "string") {
|
||||
let markdownContent = mdService.toMarkdown(content);
|
||||
|
||||
if (markdownContent.trim().length > 0 && !markdownContent.startsWith("# ")) {
|
||||
@@ -369,8 +361,12 @@ ${markdownContent}`;
|
||||
}
|
||||
|
||||
const note = becca.getNote(noteMeta.noteId);
|
||||
if (!note) { throw new Error("Unable to find note."); }
|
||||
if (!note.utcDateModified) { throw new Error("Unable to find modification date."); }
|
||||
if (!note) {
|
||||
throw new Error("Unable to find note.");
|
||||
}
|
||||
if (!note.utcDateModified) {
|
||||
throw new Error("Unable to find modification date.");
|
||||
}
|
||||
|
||||
if (noteMeta.dataFileName) {
|
||||
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
|
||||
@@ -384,7 +380,9 @@ ${markdownContent}`;
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
for (const attachmentMeta of noteMeta.attachments || []) {
|
||||
if (!attachmentMeta.attachmentId) { continue; }
|
||||
if (!attachmentMeta.attachmentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const attachment = note.getAttachmentById(attachmentMeta.attachmentId);
|
||||
const content = attachment.getContent();
|
||||
@@ -399,7 +397,7 @@ ${markdownContent}`;
|
||||
const directoryPath = filePathPrefix + noteMeta.dirFileName;
|
||||
|
||||
// create directory
|
||||
archive.append('', { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) });
|
||||
archive.append("", { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) });
|
||||
|
||||
for (const childMeta of noteMeta.children || []) {
|
||||
saveNote(childMeta, `${directoryPath}/`);
|
||||
@@ -409,27 +407,26 @@ ${markdownContent}`;
|
||||
|
||||
function saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) {
|
||||
function saveNavigationInner(meta: NoteMeta) {
|
||||
let html = '<li>';
|
||||
let html = "<li>";
|
||||
|
||||
const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ''}${meta.title}`);
|
||||
const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`);
|
||||
|
||||
if (meta.dataFileName && meta.noteId) {
|
||||
const targetUrl = getNoteTargetUrl(meta.noteId, rootMeta);
|
||||
|
||||
html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
html += escapedTitle;
|
||||
}
|
||||
|
||||
if (meta.children && meta.children.length > 0) {
|
||||
html += '<ul>';
|
||||
html += "<ul>";
|
||||
|
||||
for (const child of meta.children) {
|
||||
html += saveNavigationInner(child);
|
||||
}
|
||||
|
||||
html += '</ul>'
|
||||
html += "</ul>";
|
||||
}
|
||||
|
||||
return `${html}</li>`;
|
||||
@@ -444,9 +441,7 @@ ${markdownContent}`;
|
||||
<ul>${saveNavigationInner(rootMeta)}</ul>
|
||||
</body>
|
||||
</html>`;
|
||||
const prettyHtml = fullHtml.length < 100_000
|
||||
? html.prettyPrint(fullHtml, {indent_size: 2})
|
||||
: fullHtml;
|
||||
const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml;
|
||||
|
||||
archive.append(prettyHtml, { name: navigationMeta.dataFileName });
|
||||
}
|
||||
@@ -462,8 +457,7 @@ ${markdownContent}`;
|
||||
|
||||
if (curMeta.children && curMeta.children.length > 0) {
|
||||
curMeta = curMeta.children[0];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -489,20 +483,20 @@ ${markdownContent}`;
|
||||
archive.append(cssContent, { name: cssMeta.dataFileName });
|
||||
}
|
||||
|
||||
const existingFileNames: Record<string, number> = format === 'html' ? {'navigation': 0, 'index': 1} : {};
|
||||
const existingFileNames: Record<string, number> = format === "html" ? { navigation: 0, index: 1 } : {};
|
||||
const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
|
||||
|
||||
const metaFile = {
|
||||
formatVersion: 2,
|
||||
appVersion: packageInfo.version,
|
||||
files: [ rootMeta ]
|
||||
files: [rootMeta]
|
||||
};
|
||||
|
||||
let navigationMeta: NoteMeta | null = null;
|
||||
let indexMeta: NoteMeta | null = null;
|
||||
let cssMeta: NoteMeta | null = null;
|
||||
|
||||
if (format === 'html') {
|
||||
if (format === "html") {
|
||||
navigationMeta = {
|
||||
noImport: true,
|
||||
dataFileName: "navigation.html"
|
||||
@@ -527,12 +521,12 @@ ${markdownContent}`;
|
||||
|
||||
for (const noteMeta of Object.values(noteIdToMeta)) {
|
||||
// filter out relations which are not inside this export
|
||||
noteMeta.attributes = (noteMeta.attributes || []).filter(attr => {
|
||||
if (attr.type !== 'relation') {
|
||||
noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => {
|
||||
if (attr.type !== "relation") {
|
||||
return true;
|
||||
} else if (attr.value in noteIdToMeta) {
|
||||
return true;
|
||||
} else if (attr.value === 'root' || attr.value?.startsWith("_")) {
|
||||
} else if (attr.value === "root" || attr.value?.startsWith("_")) {
|
||||
// relations to "named" noteIds can be preserved
|
||||
return true;
|
||||
} else {
|
||||
@@ -541,20 +535,21 @@ ${markdownContent}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (!rootMeta) { // corner case of disabled export for exported note
|
||||
if (!rootMeta) {
|
||||
// corner case of disabled export for exported note
|
||||
if ("sendStatus" in res) {
|
||||
res.sendStatus(400);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const metaFileJson = JSON.stringify(metaFile, null, '\t');
|
||||
const metaFileJson = JSON.stringify(metaFile, null, "\t");
|
||||
|
||||
archive.append(metaFileJson, { name: "!!!meta.json" });
|
||||
|
||||
saveNote(rootMeta, '');
|
||||
saveNote(rootMeta, "");
|
||||
|
||||
if (format === 'html') {
|
||||
if (format === "html") {
|
||||
if (!navigationMeta || !indexMeta || !cssMeta) {
|
||||
throw new Error("Missing meta.");
|
||||
}
|
||||
@@ -568,8 +563,8 @@ ${markdownContent}`;
|
||||
const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`;
|
||||
|
||||
if (setHeaders && "setHeader" in res) {
|
||||
res.setHeader('Content-Disposition', getContentDisposition(zipFileName));
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader("Content-Disposition", getContentDisposition(zipFileName));
|
||||
res.setHeader("Content-Type", "application/zip");
|
||||
}
|
||||
|
||||
archive.pipe(res);
|
||||
@@ -580,7 +575,7 @@ ${markdownContent}`;
|
||||
|
||||
async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string) {
|
||||
const fileOutputStream = fs.createWriteStream(zipFilePath);
|
||||
const taskContext = new TaskContext('no-progress-reporting');
|
||||
const taskContext = new TaskContext("no-progress-reporting");
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import hiddenSubtreeService from "./hidden_subtree.js";
|
||||
import oneTimeTimer from "./one_time_timer.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
|
||||
import { DefinitionObject } from './promoted_attribute_definition_interface.js';
|
||||
import { DefinitionObject } from "./promoted_attribute_definition_interface.js";
|
||||
|
||||
type Handler = (definition: DefinitionObject, note: BNote, targetNote: BNote) => void;
|
||||
|
||||
@@ -19,9 +19,10 @@ function runAttachedRelations(note: BNote, relationName: string, originEntity: A
|
||||
|
||||
// the same script note can get here with multiple ways, but execute only once
|
||||
const notesToRun = new Set(
|
||||
note.getRelations(relationName)
|
||||
.map(relation => relation.getTargetNote())
|
||||
.filter(note => !!note) as BNote[]
|
||||
note
|
||||
.getRelations(relationName)
|
||||
.map((relation) => relation.getTargetNote())
|
||||
.filter((note) => !!note) as BNote[]
|
||||
);
|
||||
|
||||
for (const noteToRun of notesToRun) {
|
||||
@@ -29,8 +30,8 @@ function runAttachedRelations(note: BNote, relationName: string, originEntity: A
|
||||
}
|
||||
}
|
||||
|
||||
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
|
||||
runAttachedRelations(note, 'runOnNoteTitleChange', note);
|
||||
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, (note) => {
|
||||
runAttachedRelations(note, "runOnNoteTitleChange", note);
|
||||
|
||||
if (!note.isRoot()) {
|
||||
const noteFromCache = becca.notes[note.noteId];
|
||||
@@ -48,23 +49,22 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
|
||||
});
|
||||
|
||||
eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED], ({ entityName, entity }) => {
|
||||
if (entityName === 'attributes') {
|
||||
runAttachedRelations(entity.getNote(), 'runOnAttributeChange', entity);
|
||||
if (entityName === "attributes") {
|
||||
runAttachedRelations(entity.getNote(), "runOnAttributeChange", entity);
|
||||
|
||||
if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale'].includes(entity.name)) {
|
||||
if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) {
|
||||
handleSortedAttribute(entity);
|
||||
} else if (entity.type === 'label') {
|
||||
} else if (entity.type === "label") {
|
||||
handleMaybeSortingLabel(entity);
|
||||
}
|
||||
}
|
||||
else if (entityName === 'notes') {
|
||||
} else if (entityName === "notes") {
|
||||
// ENTITY_DELETED won't trigger anything since all branches/attributes are already deleted at this point
|
||||
runAttachedRelations(entity, 'runOnNoteChange', entity);
|
||||
runAttachedRelations(entity, "runOnNoteChange", entity);
|
||||
}
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => {
|
||||
if (entityName === 'branches') {
|
||||
if (entityName === "branches") {
|
||||
const parentNote = becca.getNote(entity.parentNoteId);
|
||||
|
||||
if (parentNote?.hasLabel("sorted")) {
|
||||
@@ -74,20 +74,20 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) =>
|
||||
const childNote = becca.getNote(entity.noteId);
|
||||
|
||||
if (childNote) {
|
||||
runAttachedRelations(childNote, 'runOnBranchChange', entity);
|
||||
runAttachedRelations(childNote, "runOnBranchChange", entity);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.NOTE_CONTENT_CHANGE, ({ entity }) => {
|
||||
runAttachedRelations(entity, 'runOnNoteContentChange', entity);
|
||||
runAttachedRelations(entity, "runOnNoteContentChange", entity);
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => {
|
||||
if (entityName === 'attributes') {
|
||||
runAttachedRelations(entity.getNote(), 'runOnAttributeCreation', entity);
|
||||
if (entityName === "attributes") {
|
||||
runAttachedRelations(entity.getNote(), "runOnAttributeCreation", entity);
|
||||
|
||||
if (entity.type === 'relation' && entity.name === 'template') {
|
||||
if (entity.type === "relation" && entity.name === "template") {
|
||||
const note = becca.getNote(entity.noteId);
|
||||
if (!note) {
|
||||
return;
|
||||
@@ -101,12 +101,13 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
||||
|
||||
const content = note.getContent();
|
||||
|
||||
if (["text", "code"].includes(note.type)
|
||||
&& typeof content === "string"
|
||||
if (
|
||||
["text", "code"].includes(note.type) &&
|
||||
typeof content === "string" &&
|
||||
// if the note has already content we're not going to overwrite it with template's one
|
||||
&& (!content || content.trim().length === 0)
|
||||
&& templateNote.hasStringContent()) {
|
||||
|
||||
(!content || content.trim().length === 0) &&
|
||||
templateNote.hasStringContent()
|
||||
) {
|
||||
const templateNoteContent = templateNote.getContent();
|
||||
|
||||
if (templateNoteContent) {
|
||||
@@ -123,32 +124,28 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
||||
if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) {
|
||||
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
|
||||
}
|
||||
}
|
||||
else if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale'].includes(entity.name)) {
|
||||
} else if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) {
|
||||
handleSortedAttribute(entity);
|
||||
}
|
||||
else if (entity.type === 'label') {
|
||||
} else if (entity.type === "label") {
|
||||
handleMaybeSortingLabel(entity);
|
||||
}
|
||||
}
|
||||
else if (entityName === 'branches') {
|
||||
runAttachedRelations(entity.getNote(), 'runOnBranchCreation', entity);
|
||||
} else if (entityName === "branches") {
|
||||
runAttachedRelations(entity.getNote(), "runOnBranchCreation", entity);
|
||||
|
||||
if (entity.parentNote?.hasLabel("sorted")) {
|
||||
treeService.sortNotesIfNeeded(entity.parentNoteId);
|
||||
}
|
||||
}
|
||||
else if (entityName === 'notes') {
|
||||
runAttachedRelations(entity, 'runOnNoteCreation', entity);
|
||||
} else if (entityName === "notes") {
|
||||
runAttachedRelations(entity, "runOnNoteCreation", entity);
|
||||
}
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote }) => {
|
||||
runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
|
||||
runAttachedRelations(parentNote, "runOnChildNoteCreation", childNote);
|
||||
});
|
||||
|
||||
function processInverseRelations(entityName: string, entity: BAttribute, handler: Handler) {
|
||||
if (entityName === 'attributes' && entity.type === 'relation') {
|
||||
if (entityName === "attributes" && entity.type === "relation") {
|
||||
const note = entity.getNote();
|
||||
const relDefinitions = note.getLabels(`relation:${entity.name}`);
|
||||
|
||||
@@ -194,9 +191,11 @@ function handleMaybeSortingLabel(entity: BAttribute) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sorted.includes(entity.name) // hacky check if this label is used in the sort
|
||||
|| entity.name === "top"
|
||||
|| entity.name === "bottom") {
|
||||
if (
|
||||
sorted.includes(entity.name) || // hacky check if this label is used in the sort
|
||||
entity.name === "top" ||
|
||||
entity.name === "bottom"
|
||||
) {
|
||||
treeService.sortNotesIfNeeded(parentNote.noteId);
|
||||
}
|
||||
}
|
||||
@@ -207,13 +206,12 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) =>
|
||||
processInverseRelations(entityName, entity, (definition, note, targetNote) => {
|
||||
// we need to make sure that also target's inverse attribute exists and if not, then create it
|
||||
// inverse attribute has to target our note as well
|
||||
const hasInverseAttribute = (targetNote.getRelations(definition.inverseRelation))
|
||||
.some(attr => attr.value === note.noteId);
|
||||
const hasInverseAttribute = targetNote.getRelations(definition.inverseRelation).some((attr) => attr.value === note.noteId);
|
||||
|
||||
if (!hasInverseAttribute) {
|
||||
new BAttribute({
|
||||
noteId: targetNote.noteId,
|
||||
type: 'relation',
|
||||
type: "relation",
|
||||
name: definition.inverseRelation || "",
|
||||
value: note.noteId,
|
||||
isInheritable: entity.isInheritable
|
||||
@@ -237,15 +235,14 @@ eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) =>
|
||||
}
|
||||
});
|
||||
|
||||
if (entityName === 'branches') {
|
||||
runAttachedRelations(entity.getNote(), 'runOnBranchDeletion', entity);
|
||||
if (entityName === "branches") {
|
||||
runAttachedRelations(entity.getNote(), "runOnBranchDeletion", entity);
|
||||
}
|
||||
|
||||
if (entityName === 'notes' && entity.noteId.startsWith("_")) {
|
||||
if (entityName === "notes" && entity.noteId.startsWith("_")) {
|
||||
// "named" note has been deleted, we will probably need to rebuild the hidden subtree
|
||||
// scheduling so that bulk deletes won't trigger so many checks
|
||||
oneTimeTimer.scheduleExecution('hidden-subtree-check', 1000,
|
||||
() => hiddenSubtreeService.checkHiddenSubtree());
|
||||
oneTimeTimer.scheduleExecution("hidden-subtree-check", 1000, () => hiddenSubtreeService.checkHiddenSubtree());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ interface Attribute {
|
||||
type: AttributeType;
|
||||
name: string;
|
||||
isInheritable?: boolean;
|
||||
value?: string
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
@@ -58,253 +58,292 @@ let hiddenSubtreeDefinition: Item;
|
||||
|
||||
function buildHiddenSubtreeDefinition(): Item {
|
||||
return {
|
||||
id: '_hidden',
|
||||
id: "_hidden",
|
||||
title: t("hidden-subtree.root-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx bx-hide',
|
||||
type: "doc",
|
||||
icon: "bx bx-hide",
|
||||
// we want to keep the hidden subtree always last, otherwise there will be problems with e.g., keyboard navigation
|
||||
// over tree when it's in the middle
|
||||
notePosition: 999_999_999,
|
||||
attributes: [
|
||||
{ type: 'label', name: 'excludeFromNoteMap', isInheritable: true },
|
||||
{ type: 'label', name: 'docName', value: 'hidden' }
|
||||
{ type: "label", name: "excludeFromNoteMap", isInheritable: true },
|
||||
{ type: "label", name: "docName", value: "hidden" }
|
||||
],
|
||||
children: [
|
||||
{
|
||||
id: '_search',
|
||||
id: "_search",
|
||||
title: t("hidden-subtree.search-history-title"),
|
||||
type: 'doc'
|
||||
type: "doc"
|
||||
},
|
||||
{
|
||||
id: '_globalNoteMap',
|
||||
id: "_globalNoteMap",
|
||||
title: t("hidden-subtree.note-map-title"),
|
||||
type: 'noteMap',
|
||||
type: "noteMap",
|
||||
attributes: [
|
||||
{ type: 'label', name: 'mapRootNoteId', value: 'hoisted' },
|
||||
{ type: 'label', name: 'keepCurrentHoisting' }
|
||||
{ type: "label", name: "mapRootNoteId", value: "hoisted" },
|
||||
{ type: "label", name: "keepCurrentHoisting" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '_sqlConsole',
|
||||
id: "_sqlConsole",
|
||||
title: t("hidden-subtree.sql-console-history-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx-data'
|
||||
type: "doc",
|
||||
icon: "bx-data"
|
||||
},
|
||||
{
|
||||
id: '_share',
|
||||
id: "_share",
|
||||
title: t("hidden-subtree.shared-notes-title"),
|
||||
type: 'doc',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'share' } ]
|
||||
type: "doc",
|
||||
attributes: [{ type: "label", name: "docName", value: "share" }]
|
||||
},
|
||||
{
|
||||
id: '_bulkAction',
|
||||
id: "_bulkAction",
|
||||
title: t("hidden-subtree.bulk-action-title"),
|
||||
type: 'doc',
|
||||
type: "doc"
|
||||
},
|
||||
{
|
||||
id: '_backendLog',
|
||||
id: "_backendLog",
|
||||
title: t("hidden-subtree.backend-log-title"),
|
||||
type: 'contentWidget',
|
||||
icon: 'bx-terminal',
|
||||
type: "contentWidget",
|
||||
icon: "bx-terminal",
|
||||
attributes: [
|
||||
{ type: 'label', name: 'keepCurrentHoisting' },
|
||||
{ type: 'label', name: 'fullContentWidth' }
|
||||
{ type: "label", name: "keepCurrentHoisting" },
|
||||
{ type: "label", name: "fullContentWidth" }
|
||||
]
|
||||
},
|
||||
{
|
||||
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
|
||||
id: '_userHidden',
|
||||
id: "_userHidden",
|
||||
title: t("hidden-subtree.user-hidden-title"),
|
||||
type: 'doc',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'user_hidden' } ]
|
||||
type: "doc",
|
||||
attributes: [{ type: "label", name: "docName", value: "user_hidden" }]
|
||||
},
|
||||
{
|
||||
id: LBTPL_ROOT,
|
||||
title: t("hidden-subtree.launch-bar-templates-title"),
|
||||
type: 'doc',
|
||||
type: "doc",
|
||||
children: [
|
||||
{
|
||||
id: LBTPL_BASE,
|
||||
title: t("hidden-subtree.base-abstract-launcher-title"),
|
||||
type: 'doc'
|
||||
type: "doc"
|
||||
},
|
||||
{
|
||||
id: LBTPL_COMMAND,
|
||||
title: t("hidden-subtree.command-launcher-title"),
|
||||
type: 'doc',
|
||||
type: "doc",
|
||||
attributes: [
|
||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
||||
{ type: 'label', name: 'launcherType', value: 'command' },
|
||||
{ type: 'label', name: 'docName', value: 'launchbar_command_launcher' }
|
||||
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||
{ type: "label", name: "launcherType", value: "command" },
|
||||
{ type: "label", name: "docName", value: "launchbar_command_launcher" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: LBTPL_NOTE_LAUNCHER,
|
||||
title: t("hidden-subtree.note-launcher-title"),
|
||||
type: 'doc',
|
||||
type: "doc",
|
||||
attributes: [
|
||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
||||
{ type: 'label', name: 'launcherType', value: 'note' },
|
||||
{ type: 'label', name: 'relation:target', value: 'promoted' },
|
||||
{ type: 'label', name: 'relation:hoistedNote', value: 'promoted' },
|
||||
{ type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
|
||||
{ type: 'label', name: 'docName', value: 'launchbar_note_launcher' }
|
||||
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||
{ type: "label", name: "launcherType", value: "note" },
|
||||
{ type: "label", name: "relation:target", value: "promoted" },
|
||||
{ type: "label", name: "relation:hoistedNote", value: "promoted" },
|
||||
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
|
||||
{ type: "label", name: "docName", value: "launchbar_note_launcher" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: LBTPL_SCRIPT,
|
||||
title: t("hidden-subtree.script-launcher-title"),
|
||||
type: 'doc',
|
||||
type: "doc",
|
||||
attributes: [
|
||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
||||
{ type: 'label', name: 'launcherType', value: 'script' },
|
||||
{ type: 'label', name: 'relation:script', value: 'promoted' },
|
||||
{ type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
|
||||
{ type: 'label', name: 'docName', value: 'launchbar_script_launcher' }
|
||||
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||
{ type: "label", name: "launcherType", value: "script" },
|
||||
{ type: "label", name: "relation:script", value: "promoted" },
|
||||
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
|
||||
{ type: "label", name: "docName", value: "launchbar_script_launcher" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: LBTPL_BUILTIN_WIDGET,
|
||||
title: t("hidden-subtree.built-in-widget-title"),
|
||||
type: 'doc',
|
||||
type: "doc",
|
||||
attributes: [
|
||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
||||
{ type: 'label', name: 'launcherType', value: 'builtinWidget' }
|
||||
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||
{ type: "label", name: "launcherType", value: "builtinWidget" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: LBTPL_SPACER,
|
||||
title: t("hidden-subtree.spacer-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx-move-vertical',
|
||||
type: "doc",
|
||||
icon: "bx-move-vertical",
|
||||
attributes: [
|
||||
{ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET },
|
||||
{ type: 'label', name: 'builtinWidget', value: 'spacer' },
|
||||
{ type: 'label', name: 'label:baseSize', value: 'promoted,number' },
|
||||
{ type: 'label', name: 'label:growthFactor', value: 'promoted,number' },
|
||||
{ type: 'label', name: 'docName', value: 'launchbar_spacer' }
|
||||
{ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET },
|
||||
{ type: "label", name: "builtinWidget", value: "spacer" },
|
||||
{ type: "label", name: "label:baseSize", value: "promoted,number" },
|
||||
{ type: "label", name: "label:growthFactor", value: "promoted,number" },
|
||||
{ type: "label", name: "docName", value: "launchbar_spacer" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: LBTPL_CUSTOM_WIDGET,
|
||||
title: t("hidden-subtree.custom-widget-title"),
|
||||
type: 'doc',
|
||||
type: "doc",
|
||||
attributes: [
|
||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
||||
{ type: 'label', name: 'launcherType', value: 'customWidget' },
|
||||
{ type: 'label', name: 'relation:widget', value: 'promoted' },
|
||||
{ type: 'label', name: 'docName', value: 'launchbar_widget_launcher' }
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '_lbRoot',
|
||||
title: t("hidden-subtree.launch-bar-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx-sidebar',
|
||||
isExpanded: true,
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
||||
children: [
|
||||
{
|
||||
id: '_lbAvailableLaunchers',
|
||||
title: t("hidden-subtree.available-launchers-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx-hide',
|
||||
isExpanded: true,
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
||||
children: [
|
||||
{ id: '_lbBackInHistory', title: t("hidden-subtree.go-to-previous-note-title"), type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-chevron-left',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
|
||||
{ id: '_lbForwardInHistory', title: t("hidden-subtree.go-to-next-note-title"), type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-chevron-right',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
|
||||
{ id: '_lbBackendLog', title: t("hidden-subtree.backend-log-title"), type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '_lbVisibleLaunchers',
|
||||
title: t("hidden-subtree.visible-launchers-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx-show',
|
||||
isExpanded: true,
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
||||
children: [
|
||||
{ id: '_lbNewNote', title: t("hidden-subtree.new-note-title"), type: 'launcher', command: 'createNoteIntoInbox', icon: 'bx bx-file-blank' },
|
||||
{ id: '_lbSearch', title: t("hidden-subtree.search-notes-title"), type: 'launcher', command: 'searchNotes', icon: 'bx bx-search', attributes: [
|
||||
{ type: 'label', name: 'desktopOnly' }
|
||||
] },
|
||||
{ id: '_lbJumpTo', title: t("hidden-subtree.jump-to-note-title"), type: 'launcher', command: 'jumpToNote', icon: 'bx bx-send', attributes: [
|
||||
{ type: 'label', name: 'desktopOnly' }
|
||||
] },
|
||||
{ id: '_lbNoteMap', title: t("hidden-subtree.note-map-title"), type: 'launcher', targetNoteId: '_globalNoteMap', icon: 'bx bxs-network-chart' },
|
||||
{ id: '_lbCalendar', title: t("hidden-subtree.calendar-title"), type: 'launcher', builtinWidget: 'calendar', icon: 'bx bx-calendar' },
|
||||
{ id: '_lbRecentChanges', title: t("hidden-subtree.recent-changes-title"), type: 'launcher', command: 'showRecentChanges', icon: 'bx bx-history', attributes: [
|
||||
{ type: 'label', name: 'desktopOnly' }
|
||||
] },
|
||||
{ id: '_lbSpacer1', title: t("hidden-subtree.spacer-title"), type: 'launcher', builtinWidget: 'spacer', baseSize: "50", growthFactor: "0" },
|
||||
{ id: '_lbBookmarks', title: t("hidden-subtree.bookmarks-title"), type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
|
||||
{ id: '_lbToday', title: t("hidden-subtree.open-today-journal-note-title"), type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' },
|
||||
{ id: '_lbSpacer2', title: t("hidden-subtree.spacer-title"), type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
|
||||
{ id: '_lbQuickSearch', title: t("hidden-subtree.quick-search-title"), type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
|
||||
{ id: '_lbProtectedSession', title: t("hidden-subtree.protected-session-title"), type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
|
||||
{ id: '_lbSyncStatus', title: t("hidden-subtree.sync-status-title"), type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' },
|
||||
{ id: '_lbSettings', title: t("hidden-subtree.settings-title"), type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' }
|
||||
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||
{ type: "label", name: "launcherType", value: "customWidget" },
|
||||
{ type: "label", name: "relation:widget", value: "promoted" },
|
||||
{ type: "label", name: "docName", value: "launchbar_widget_launcher" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '_lbMobileRoot',
|
||||
id: "_lbRoot",
|
||||
title: t("hidden-subtree.launch-bar-title"),
|
||||
type: "doc",
|
||||
icon: "bx-sidebar",
|
||||
isExpanded: true,
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
|
||||
children: [
|
||||
{
|
||||
id: "_lbAvailableLaunchers",
|
||||
title: t("hidden-subtree.available-launchers-title"),
|
||||
type: "doc",
|
||||
icon: "bx-hide",
|
||||
isExpanded: true,
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
|
||||
children: [
|
||||
{
|
||||
id: "_lbBackInHistory",
|
||||
title: t("hidden-subtree.go-to-previous-note-title"),
|
||||
type: "launcher",
|
||||
builtinWidget: "backInHistoryButton",
|
||||
icon: "bx bxs-chevron-left",
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
|
||||
},
|
||||
{
|
||||
id: "_lbForwardInHistory",
|
||||
title: t("hidden-subtree.go-to-next-note-title"),
|
||||
type: "launcher",
|
||||
builtinWidget: "forwardInHistoryButton",
|
||||
icon: "bx bxs-chevron-right",
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
|
||||
},
|
||||
{ id: "_lbBackendLog", title: t("hidden-subtree.backend-log-title"), type: "launcher", targetNoteId: "_backendLog", icon: "bx bx-terminal" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_lbVisibleLaunchers",
|
||||
title: t("hidden-subtree.visible-launchers-title"),
|
||||
type: "doc",
|
||||
icon: "bx-show",
|
||||
isExpanded: true,
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
|
||||
children: [
|
||||
{ id: "_lbNewNote", title: t("hidden-subtree.new-note-title"), type: "launcher", command: "createNoteIntoInbox", icon: "bx bx-file-blank" },
|
||||
{
|
||||
id: "_lbSearch",
|
||||
title: t("hidden-subtree.search-notes-title"),
|
||||
type: "launcher",
|
||||
command: "searchNotes",
|
||||
icon: "bx bx-search",
|
||||
attributes: [{ type: "label", name: "desktopOnly" }]
|
||||
},
|
||||
{
|
||||
id: "_lbJumpTo",
|
||||
title: t("hidden-subtree.jump-to-note-title"),
|
||||
type: "launcher",
|
||||
command: "jumpToNote",
|
||||
icon: "bx bx-send",
|
||||
attributes: [{ type: "label", name: "desktopOnly" }]
|
||||
},
|
||||
{ id: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" },
|
||||
{ id: "_lbCalendar", title: t("hidden-subtree.calendar-title"), type: "launcher", builtinWidget: "calendar", icon: "bx bx-calendar" },
|
||||
{
|
||||
id: "_lbRecentChanges",
|
||||
title: t("hidden-subtree.recent-changes-title"),
|
||||
type: "launcher",
|
||||
command: "showRecentChanges",
|
||||
icon: "bx bx-history",
|
||||
attributes: [{ type: "label", name: "desktopOnly" }]
|
||||
},
|
||||
{ id: "_lbSpacer1", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "50", growthFactor: "0" },
|
||||
{ id: "_lbBookmarks", title: t("hidden-subtree.bookmarks-title"), type: "launcher", builtinWidget: "bookmarks", icon: "bx bx-bookmark" },
|
||||
{ id: "_lbToday", title: t("hidden-subtree.open-today-journal-note-title"), type: "launcher", builtinWidget: "todayInJournal", icon: "bx bx-calendar-star" },
|
||||
{ id: "_lbSpacer2", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "0", growthFactor: "1" },
|
||||
{ id: "_lbQuickSearch", title: t("hidden-subtree.quick-search-title"), type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
|
||||
{ id: "_lbProtectedSession", title: t("hidden-subtree.protected-session-title"), type: "launcher", builtinWidget: "protectedSession", icon: "bx bx bx-shield-quarter" },
|
||||
{ id: "_lbSyncStatus", title: t("hidden-subtree.sync-status-title"), type: "launcher", builtinWidget: "syncStatus", icon: "bx bx-wifi" },
|
||||
{ id: "_lbSettings", title: t("hidden-subtree.settings-title"), type: "launcher", command: "showOptions", icon: "bx bx-cog" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_lbMobileRoot",
|
||||
title: "Mobile Launch Bar",
|
||||
type: "doc",
|
||||
icon: "bx-mobile",
|
||||
isExpanded: true,
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
|
||||
children: [
|
||||
{
|
||||
id: "_lbMobileAvailableLaunchers",
|
||||
title: t("hidden-subtree.available-launchers-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx-hide',
|
||||
type: "doc",
|
||||
icon: "bx-hide",
|
||||
isExpanded: true,
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: "_lbMobileVisibleLaunchers",
|
||||
title: t("hidden-subtree.visible-launchers-title"),
|
||||
type: 'doc',
|
||||
icon: 'bx-show',
|
||||
type: "doc",
|
||||
icon: "bx-show",
|
||||
isExpanded: true,
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
|
||||
children: [
|
||||
{ id: '_lbMobileBackInHistory', title: t("hidden-subtree.go-to-previous-note-title"), type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-chevron-left',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
|
||||
{ id: '_lbMobileForwardInHistory', title: t("hidden-subtree.go-to-next-note-title"), type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-chevron-right',
|
||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
|
||||
{ id: '_lbMobileJumpTo', title: t("hidden-subtree.jump-to-note-title"), type: 'launcher', command: 'jumpToNote', icon: 'bx bx-plus-circle' }
|
||||
{
|
||||
id: "_lbMobileBackInHistory",
|
||||
title: t("hidden-subtree.go-to-previous-note-title"),
|
||||
type: "launcher",
|
||||
builtinWidget: "backInHistoryButton",
|
||||
icon: "bx bxs-chevron-left",
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
|
||||
},
|
||||
{
|
||||
id: "_lbMobileForwardInHistory",
|
||||
title: t("hidden-subtree.go-to-next-note-title"),
|
||||
type: "launcher",
|
||||
builtinWidget: "forwardInHistoryButton",
|
||||
icon: "bx bxs-chevron-right",
|
||||
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
|
||||
},
|
||||
{ id: "_lbMobileJumpTo", title: t("hidden-subtree.jump-to-note-title"), type: "launcher", command: "jumpToNote", icon: "bx bx-plus-circle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '_options',
|
||||
id: "_options",
|
||||
title: t("hidden-subtree.options-title"),
|
||||
type: 'book',
|
||||
icon: 'bx-cog',
|
||||
type: "book",
|
||||
icon: "bx-cog",
|
||||
children: [
|
||||
{ id: '_optionsAppearance', title: t("hidden-subtree.appearance-title"), type: 'contentWidget', icon: 'bx-layout' },
|
||||
{ id: '_optionsShortcuts', title: t("hidden-subtree.shortcuts-title"), type: 'contentWidget', icon: 'bxs-keyboard' },
|
||||
{ id: '_optionsTextNotes', title: t("hidden-subtree.text-notes"), type: 'contentWidget', icon: 'bx-text' },
|
||||
{ id: '_optionsCodeNotes', title: t("hidden-subtree.code-notes-title"), type: 'contentWidget', icon: 'bx-code' },
|
||||
{ id: '_optionsImages', title: t("hidden-subtree.images-title"), type: 'contentWidget', icon: 'bx-image' },
|
||||
{ id: '_optionsSpellcheck', title: t("hidden-subtree.spellcheck-title"), type: 'contentWidget', icon: 'bx-check-double' },
|
||||
{ id: '_optionsPassword', title: t("hidden-subtree.password-title"), type: 'contentWidget', icon: 'bx-lock' },
|
||||
{ id: '_optionsEtapi', title: t("hidden-subtree.etapi-title"), type: 'contentWidget', icon: 'bx-extension' },
|
||||
{ id: '_optionsBackup', title: t("hidden-subtree.backup-title"), type: 'contentWidget', icon: 'bx-data' },
|
||||
{ id: '_optionsSync', title: t("hidden-subtree.sync-title"), type: 'contentWidget', icon: 'bx-wifi' },
|
||||
{ id: '_optionsOther', title: t("hidden-subtree.other"), type: 'contentWidget', icon: 'bx-dots-horizontal' },
|
||||
{ id: '_optionsAdvanced', title: t("hidden-subtree.advanced-title"), type: 'contentWidget' }
|
||||
{ id: "_optionsAppearance", title: t("hidden-subtree.appearance-title"), type: "contentWidget", icon: "bx-layout" },
|
||||
{ id: "_optionsShortcuts", title: t("hidden-subtree.shortcuts-title"), type: "contentWidget", icon: "bxs-keyboard" },
|
||||
{ id: "_optionsTextNotes", title: t("hidden-subtree.text-notes"), type: "contentWidget", icon: "bx-text" },
|
||||
{ id: "_optionsCodeNotes", title: t("hidden-subtree.code-notes-title"), type: "contentWidget", icon: "bx-code" },
|
||||
{ id: "_optionsImages", title: t("hidden-subtree.images-title"), type: "contentWidget", icon: "bx-image" },
|
||||
{ id: "_optionsSpellcheck", title: t("hidden-subtree.spellcheck-title"), type: "contentWidget", icon: "bx-check-double" },
|
||||
{ id: "_optionsPassword", title: t("hidden-subtree.password-title"), type: "contentWidget", icon: "bx-lock" },
|
||||
{ id: "_optionsEtapi", title: t("hidden-subtree.etapi-title"), type: "contentWidget", icon: "bx-extension" },
|
||||
{ id: "_optionsBackup", title: t("hidden-subtree.backup-title"), type: "contentWidget", icon: "bx-data" },
|
||||
{ id: "_optionsSync", title: t("hidden-subtree.sync-title"), type: "contentWidget", icon: "bx-wifi" },
|
||||
{ id: "_optionsOther", title: t("hidden-subtree.other"), type: "contentWidget", icon: "bx-dots-horizontal" },
|
||||
{ id: "_optionsAdvanced", title: t("hidden-subtree.advanced-title"), type: "contentWidget" }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -326,7 +365,7 @@ function checkHiddenSubtree(force = false, extraOpts: CheckHiddenExtraOpts = {})
|
||||
hiddenSubtreeDefinition = buildHiddenSubtreeDefinition();
|
||||
}
|
||||
|
||||
checkHiddenSubtreeRecursively('root', hiddenSubtreeDefinition, extraOpts);
|
||||
checkHiddenSubtreeRecursively("root", hiddenSubtreeDefinition, extraOpts);
|
||||
}
|
||||
|
||||
function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOpts: CheckHiddenExtraOpts = {}) {
|
||||
@@ -334,7 +373,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
|
||||
throw new Error(`Item does not contain mandatory properties: ${JSON.stringify(item)}`);
|
||||
}
|
||||
|
||||
if (item.id.charAt(0) !== '_') {
|
||||
if (item.id.charAt(0) !== "_") {
|
||||
throw new Error(`ID has to start with underscore, given '${item.id}'`);
|
||||
}
|
||||
|
||||
@@ -342,41 +381,41 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
|
||||
let branch;
|
||||
|
||||
if (!note) {
|
||||
({note, branch} = noteService.createNewNote({
|
||||
({ note, branch } = noteService.createNewNote({
|
||||
noteId: item.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
parentNoteId: parentNoteId,
|
||||
content: '',
|
||||
content: "",
|
||||
ignoreForbiddenParents: true
|
||||
}));
|
||||
} else {
|
||||
branch = note.getParentBranches().find(branch => branch.parentNoteId === parentNoteId);
|
||||
branch = note.getParentBranches().find((branch) => branch.parentNoteId === parentNoteId);
|
||||
}
|
||||
|
||||
const attrs = [...(item.attributes || [])];
|
||||
|
||||
if (item.icon) {
|
||||
attrs.push({ type: 'label', name: 'iconClass', value: `bx ${item.icon}` });
|
||||
attrs.push({ type: "label", name: "iconClass", value: `bx ${item.icon}` });
|
||||
}
|
||||
|
||||
if (item.type === 'launcher') {
|
||||
if (item.type === "launcher") {
|
||||
if (item.command) {
|
||||
attrs.push({ type: 'relation', name: 'template', value: LBTPL_COMMAND });
|
||||
attrs.push({ type: 'label', name: 'command', value: item.command });
|
||||
attrs.push({ type: "relation", name: "template", value: LBTPL_COMMAND });
|
||||
attrs.push({ type: "label", name: "command", value: item.command });
|
||||
} else if (item.builtinWidget) {
|
||||
if (item.builtinWidget === 'spacer') {
|
||||
attrs.push({ type: 'relation', name: 'template', value: LBTPL_SPACER });
|
||||
attrs.push({ type: 'label', name: 'baseSize', value: item.baseSize });
|
||||
attrs.push({ type: 'label', name: 'growthFactor', value: item.growthFactor });
|
||||
if (item.builtinWidget === "spacer") {
|
||||
attrs.push({ type: "relation", name: "template", value: LBTPL_SPACER });
|
||||
attrs.push({ type: "label", name: "baseSize", value: item.baseSize });
|
||||
attrs.push({ type: "label", name: "growthFactor", value: item.growthFactor });
|
||||
} else {
|
||||
attrs.push({ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET });
|
||||
attrs.push({ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET });
|
||||
}
|
||||
|
||||
attrs.push({ type: 'label', name: 'builtinWidget', value: item.builtinWidget });
|
||||
attrs.push({ type: "label", name: "builtinWidget", value: item.builtinWidget });
|
||||
} else if (item.targetNoteId) {
|
||||
attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER });
|
||||
attrs.push({ type: 'relation', name: 'target', value: item.targetNoteId });
|
||||
attrs.push({ type: "relation", name: "template", value: LBTPL_NOTE_LAUNCHER });
|
||||
attrs.push({ type: "relation", name: "target", value: item.targetNoteId });
|
||||
} else {
|
||||
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
|
||||
}
|
||||
@@ -410,7 +449,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
|
||||
for (const attr of attrs) {
|
||||
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
|
||||
|
||||
if (!note.getAttributes().find(attr => attr.attributeId === attrId)) {
|
||||
if (!note.getAttributes().find((attr) => attr.attributeId === attrId)) {
|
||||
new BAttribute({
|
||||
attributeId: attrId,
|
||||
noteId: note.noteId,
|
||||
|
||||
@@ -8,9 +8,9 @@ function getHoistedNoteId() {
|
||||
function isHoistedInHiddenSubtree() {
|
||||
const hoistedNoteId = getHoistedNoteId();
|
||||
|
||||
if (hoistedNoteId === 'root') {
|
||||
if (hoistedNoteId === "root") {
|
||||
return false;
|
||||
} else if (hoistedNoteId === '_hidden') {
|
||||
} else if (hoistedNoteId === "_hidden") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ function isHoistedInHiddenSubtree() {
|
||||
function getWorkspaceNote() {
|
||||
const hoistedNote = becca.getNote(cls.getHoistedNoteId());
|
||||
|
||||
if (hoistedNote && (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace'))) {
|
||||
if (hoistedNote && (hoistedNote.isRoot() || hoistedNote.hasLabel("workspace"))) {
|
||||
return hoistedNote;
|
||||
} else {
|
||||
return becca.getRoot();
|
||||
|
||||
@@ -7,7 +7,7 @@ function getHost() {
|
||||
return envHost;
|
||||
}
|
||||
|
||||
return config['Network']['host'] || '0.0.0.0';
|
||||
return config["Network"]["host"] || "0.0.0.0";
|
||||
}
|
||||
|
||||
export default getHost();
|
||||
|
||||
@@ -4,17 +4,101 @@ import optionService from "./options.js";
|
||||
|
||||
// Default list of allowed HTML tags
|
||||
export const DEFAULT_ALLOWED_TAGS = [
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
|
||||
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
|
||||
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
|
||||
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
|
||||
'en-media', // for ENEX import
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"blockquote",
|
||||
"p",
|
||||
"a",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"b",
|
||||
"i",
|
||||
"strong",
|
||||
"em",
|
||||
"strike",
|
||||
"s",
|
||||
"del",
|
||||
"abbr",
|
||||
"code",
|
||||
"hr",
|
||||
"br",
|
||||
"div",
|
||||
"table",
|
||||
"thead",
|
||||
"caption",
|
||||
"tbody",
|
||||
"tfoot",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
"pre",
|
||||
"section",
|
||||
"img",
|
||||
"figure",
|
||||
"figcaption",
|
||||
"span",
|
||||
"label",
|
||||
"input",
|
||||
"details",
|
||||
"summary",
|
||||
"address",
|
||||
"aside",
|
||||
"footer",
|
||||
"header",
|
||||
"hgroup",
|
||||
"main",
|
||||
"nav",
|
||||
"dl",
|
||||
"dt",
|
||||
"menu",
|
||||
"bdi",
|
||||
"bdo",
|
||||
"dfn",
|
||||
"kbd",
|
||||
"mark",
|
||||
"q",
|
||||
"time",
|
||||
"var",
|
||||
"wbr",
|
||||
"area",
|
||||
"map",
|
||||
"track",
|
||||
"video",
|
||||
"audio",
|
||||
"picture",
|
||||
"del",
|
||||
"ins",
|
||||
"en-media", // for ENEX import
|
||||
// Additional tags (https://github.com/TriliumNext/Notes/issues/567)
|
||||
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
|
||||
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
|
||||
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
|
||||
"acronym",
|
||||
"article",
|
||||
"big",
|
||||
"button",
|
||||
"cite",
|
||||
"col",
|
||||
"colgroup",
|
||||
"data",
|
||||
"dd",
|
||||
"fieldset",
|
||||
"form",
|
||||
"legend",
|
||||
"meter",
|
||||
"noscript",
|
||||
"option",
|
||||
"progress",
|
||||
"rp",
|
||||
"samp",
|
||||
"small",
|
||||
"sub",
|
||||
"sup",
|
||||
"template",
|
||||
"textarea",
|
||||
"tt"
|
||||
] as const;
|
||||
|
||||
// intended mainly as protection against XSS via import
|
||||
@@ -33,8 +117,7 @@ function sanitize(dirtyHtml: string) {
|
||||
for (let i = 1; i < 6; ++i) {
|
||||
if (lowercasedHtml.includes(`<h${i}`)) {
|
||||
transformTags[`h${i}`] = `h${i + 1}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +125,7 @@ function sanitize(dirtyHtml: string) {
|
||||
// Get allowed tags from options, with fallback to default list if option not yet set
|
||||
let allowedTags;
|
||||
try {
|
||||
allowedTags = JSON.parse(optionService.getOption('allowedHtmlTags'));
|
||||
allowedTags = JSON.parse(optionService.getOption("allowedHtmlTags"));
|
||||
} catch (e) {
|
||||
// Fallback to default list if option doesn't exist or is invalid
|
||||
allowedTags = DEFAULT_ALLOWED_TAGS;
|
||||
@@ -52,19 +135,60 @@ function sanitize(dirtyHtml: string) {
|
||||
return sanitizeHtml(dirtyHtml, {
|
||||
allowedTags,
|
||||
allowedAttributes: {
|
||||
"*": [ 'class', 'style', 'title', 'src', 'href', 'hash', 'disabled', 'align', 'alt', 'center', 'data-*' ],
|
||||
"input": [ "type", "checked" ]
|
||||
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
|
||||
input: ["type", "checked"]
|
||||
},
|
||||
// Be consistent with `allowedSchemes` in `src\public\app\services\link.js`
|
||||
allowedSchemes: [
|
||||
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
|
||||
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
|
||||
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
|
||||
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo'
|
||||
],
|
||||
nonTextTags: [
|
||||
'head'
|
||||
"http",
|
||||
"https",
|
||||
"ftp",
|
||||
"ftps",
|
||||
"mailto",
|
||||
"data",
|
||||
"evernote",
|
||||
"file",
|
||||
"facetime",
|
||||
"gemini",
|
||||
"git",
|
||||
"gopher",
|
||||
"imap",
|
||||
"irc",
|
||||
"irc6",
|
||||
"jabber",
|
||||
"jar",
|
||||
"lastfm",
|
||||
"ldap",
|
||||
"ldaps",
|
||||
"magnet",
|
||||
"message",
|
||||
"mumble",
|
||||
"nfs",
|
||||
"onenote",
|
||||
"pop",
|
||||
"rmi",
|
||||
"s3",
|
||||
"sftp",
|
||||
"skype",
|
||||
"sms",
|
||||
"spotify",
|
||||
"steam",
|
||||
"svn",
|
||||
"udp",
|
||||
"view-source",
|
||||
"vlc",
|
||||
"vnc",
|
||||
"ws",
|
||||
"wss",
|
||||
"xmpp",
|
||||
"jdbc",
|
||||
"slack",
|
||||
"tel",
|
||||
"smb",
|
||||
"zotero",
|
||||
"geo"
|
||||
],
|
||||
nonTextTags: ["head"],
|
||||
transformTags
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,34 +7,34 @@ import { getResourceDir } from "./utils.js";
|
||||
import hidden_subtree from "./hidden_subtree.js";
|
||||
|
||||
export async function initializeTranslations() {
|
||||
const resourceDir = getResourceDir();
|
||||
const resourceDir = getResourceDir();
|
||||
|
||||
// Initialize translations
|
||||
await i18next.use(Backend).init({
|
||||
lng: getCurrentLanguage(),
|
||||
fallbackLng: "en",
|
||||
ns: "server",
|
||||
backend: {
|
||||
loadPath: join(resourceDir, "translations/{{lng}}/{{ns}}.json")
|
||||
}
|
||||
});
|
||||
// Initialize translations
|
||||
await i18next.use(Backend).init({
|
||||
lng: getCurrentLanguage(),
|
||||
fallbackLng: "en",
|
||||
ns: "server",
|
||||
backend: {
|
||||
loadPath: join(resourceDir, "translations/{{lng}}/{{ns}}.json")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentLanguage() {
|
||||
let language;
|
||||
if (sql_init.isDbInitialized()) {
|
||||
language = options.getOptionOrNull("locale");
|
||||
}
|
||||
let language;
|
||||
if (sql_init.isDbInitialized()) {
|
||||
language = options.getOptionOrNull("locale");
|
||||
}
|
||||
|
||||
if (!language) {
|
||||
console.info("Language option not found, falling back to en.");
|
||||
language = "en";
|
||||
}
|
||||
if (!language) {
|
||||
console.info("Language option not found, falling back to en.");
|
||||
language = "en";
|
||||
}
|
||||
|
||||
return language;
|
||||
return language;
|
||||
}
|
||||
|
||||
export async function changeLanguage(locale: string) {
|
||||
await i18next.changeLanguage(locale);
|
||||
hidden_subtree.checkHiddenSubtree(true, { restoreNames: true });
|
||||
await i18next.changeLanguage(locale);
|
||||
hidden_subtree.checkHiddenSubtree(true, { restoreNames: true });
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
|
||||
|
||||
if (!origImageFormat || !["jpg", "png"].includes(origImageFormat.ext)) {
|
||||
shrinkImageSwitch = false;
|
||||
}
|
||||
else if (isAnimated(uploadBuffer)) {
|
||||
} else if (isAnimated(uploadBuffer)) {
|
||||
// recompression of animated images will make them static
|
||||
shrinkImageSwitch = false;
|
||||
}
|
||||
@@ -34,7 +33,7 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
|
||||
} else {
|
||||
finalImageBuffer = uploadBuffer;
|
||||
imageFormat = origImageFormat || {
|
||||
ext: 'dat'
|
||||
ext: "dat"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,17 +45,16 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
|
||||
|
||||
async function getImageType(buffer: Buffer) {
|
||||
if (isSvg(buffer.toString())) {
|
||||
return { ext: 'svg' }
|
||||
}
|
||||
else {
|
||||
return await imageType(buffer) || { ext: "jpg" }; // optimistic JPG default
|
||||
return { ext: "svg" };
|
||||
} else {
|
||||
return (await imageType(buffer)) || { ext: "jpg" }; // optimistic JPG default
|
||||
}
|
||||
}
|
||||
|
||||
function getImageMimeFromExtension(ext: string) {
|
||||
ext = ext.toLowerCase();
|
||||
|
||||
return `image/${ext === 'svg' ? 'svg+xml' : ext}`;
|
||||
return `image/${ext === "svg" ? "svg+xml" : ext}`;
|
||||
}
|
||||
|
||||
function updateImage(noteId: string, uploadBuffer: Buffer, originalName: string) {
|
||||
@@ -65,14 +63,16 @@ function updateImage(noteId: string, uploadBuffer: Buffer, originalName: string)
|
||||
originalName = htmlSanitizer.sanitize(originalName);
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note) { throw new Error("Unable to find note."); }
|
||||
if (!note) {
|
||||
throw new Error("Unable to find note.");
|
||||
}
|
||||
|
||||
note.saveRevision();
|
||||
|
||||
note.setLabel('originalFileName', originalName);
|
||||
note.setLabel("originalFileName", originalName);
|
||||
|
||||
// resizing images asynchronously since JIMP does not support sync operation
|
||||
processImage(uploadBuffer, originalName, true).then(({buffer, imageFormat}) => {
|
||||
processImage(uploadBuffer, originalName, true).then(({ buffer, imageFormat }) => {
|
||||
sql.transactional(() => {
|
||||
note.mime = getImageMimeFromExtension(imageFormat.ext);
|
||||
note.save();
|
||||
@@ -92,28 +92,30 @@ function saveImage(parentNoteId: string, uploadBuffer: Buffer, originalName: str
|
||||
|
||||
const fileName = sanitizeFilename(originalName);
|
||||
const parentNote = becca.getNote(parentNoteId);
|
||||
if (!parentNote) { throw new Error("Unable to find parent note."); }
|
||||
if (!parentNote) {
|
||||
throw new Error("Unable to find parent note.");
|
||||
}
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title: fileName,
|
||||
type: 'image',
|
||||
mime: 'unknown',
|
||||
content: '',
|
||||
type: "image",
|
||||
mime: "unknown",
|
||||
content: "",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
note.addLabel('originalFileName', originalName);
|
||||
note.addLabel("originalFileName", originalName);
|
||||
|
||||
// resizing images asynchronously since JIMP does not support sync operation
|
||||
processImage(uploadBuffer, originalName, shrinkImageSwitch).then(({buffer, imageFormat}) => {
|
||||
processImage(uploadBuffer, originalName, shrinkImageSwitch).then(({ buffer, imageFormat }) => {
|
||||
sql.transactional(() => {
|
||||
note.mime = getImageMimeFromExtension(imageFormat.ext);
|
||||
|
||||
if (!originalName.includes(".")) {
|
||||
originalName += `.${imageFormat.ext}`;
|
||||
|
||||
note.setLabel('originalFileName', originalName);
|
||||
note.setLabel("originalFileName", originalName);
|
||||
note.title = sanitizeFilename(originalName);
|
||||
}
|
||||
|
||||
@@ -141,8 +143,8 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam
|
||||
const note = becca.getNoteOrThrow(noteId);
|
||||
|
||||
let attachment = note.saveAttachment({
|
||||
role: 'image',
|
||||
mime: 'unknown',
|
||||
role: "image",
|
||||
mime: "unknown",
|
||||
title: fileName
|
||||
});
|
||||
|
||||
@@ -157,10 +159,12 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam
|
||||
}, 5000);
|
||||
|
||||
// resizing images asynchronously since JIMP does not support sync operation
|
||||
processImage(uploadBuffer, originalName, !!shrinkImageSwitch).then(({buffer, imageFormat}) => {
|
||||
processImage(uploadBuffer, originalName, !!shrinkImageSwitch).then(({ buffer, imageFormat }) => {
|
||||
sql.transactional(() => {
|
||||
// re-read, might be changed in the meantime
|
||||
if (!attachment.attachmentId) { throw new Error("Missing attachment ID."); }
|
||||
if (!attachment.attachmentId) {
|
||||
throw new Error("Missing attachment ID.");
|
||||
}
|
||||
attachment = becca.getAttachmentOrThrow(attachment.attachmentId);
|
||||
|
||||
attachment.mime = getImageMimeFromExtension(imageFormat.ext);
|
||||
@@ -178,7 +182,7 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam
|
||||
}
|
||||
|
||||
async function shrinkImage(buffer: Buffer, originalName: string) {
|
||||
let jpegQuality = optionService.getOptionInt('imageJpegQuality', 0);
|
||||
let jpegQuality = optionService.getOptionInt("imageJpegQuality", 0);
|
||||
|
||||
if (jpegQuality < 10 || jpegQuality > 100) {
|
||||
jpegQuality = 75;
|
||||
@@ -187,8 +191,7 @@ async function shrinkImage(buffer: Buffer, originalName: string) {
|
||||
let finalImageBuffer;
|
||||
try {
|
||||
finalImageBuffer = await resize(buffer, jpegQuality);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
log.error(`Failed to resize image '${originalName}', stack: ${e.stack}`);
|
||||
|
||||
finalImageBuffer = buffer;
|
||||
@@ -204,7 +207,7 @@ async function shrinkImage(buffer: Buffer, originalName: string) {
|
||||
}
|
||||
|
||||
async function resize(buffer: Buffer, quality: number) {
|
||||
const imageMaxWidthHeight = optionService.getOptionInt('imageMaxWidthHeight');
|
||||
const imageMaxWidthHeight = optionService.getOptionInt("imageMaxWidthHeight");
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
@@ -212,13 +215,12 @@ async function resize(buffer: Buffer, quality: number) {
|
||||
|
||||
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > imageMaxWidthHeight) {
|
||||
image.resize({ w: imageMaxWidthHeight });
|
||||
}
|
||||
else if (image.bitmap.height > imageMaxWidthHeight) {
|
||||
} else if (image.bitmap.height > imageMaxWidthHeight) {
|
||||
image.resize({ h: imageMaxWidthHeight });
|
||||
}
|
||||
|
||||
// when converting PNG to JPG, we lose the alpha channel, this is replaced by white to match Trilium white background
|
||||
image.background = 0xFFFFFFFF;
|
||||
image.background = 0xffffffff;
|
||||
|
||||
const resultBuffer = await image.getBuffer("image/jpeg", { quality });
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import sax from "sax";
|
||||
import stream from "stream";
|
||||
import { Throttle } from 'stream-throttle';
|
||||
import { Throttle } from "stream-throttle";
|
||||
import log from "../log.js";
|
||||
import { md5, escapeHtml, fromBase64 } from "../utils.js";
|
||||
import sql from "../sql.js";
|
||||
@@ -23,8 +23,7 @@ function parseDate(text: string) {
|
||||
text = text.replace(/[-:]/g, "");
|
||||
|
||||
// insert - and : to convert it to trilium format
|
||||
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2)
|
||||
+ " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
|
||||
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2) + " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -50,7 +49,7 @@ interface Note {
|
||||
noteId: string;
|
||||
blobId: string;
|
||||
content: string;
|
||||
resources: Resource[]
|
||||
resources: Resource[];
|
||||
}
|
||||
|
||||
let note: Partial<Note> = {};
|
||||
@@ -59,28 +58,26 @@ let resource: Resource;
|
||||
function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Promise<BNote> {
|
||||
const saxStream = sax.createStream(true);
|
||||
|
||||
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex")
|
||||
? file.originalname.substr(0, file.originalname.length - 5)
|
||||
: file.originalname;
|
||||
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex") ? file.originalname.substr(0, file.originalname.length - 5) : file.originalname;
|
||||
|
||||
// root note is new note into all ENEX/notebook's notes will be imported
|
||||
const rootNote = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title: rootNoteTitle,
|
||||
content: "",
|
||||
type: 'text',
|
||||
mime: 'text/html',
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
}).note;
|
||||
|
||||
function extractContent(content: string) {
|
||||
const openingNoteIndex = content.indexOf('<en-note>');
|
||||
const openingNoteIndex = content.indexOf("<en-note>");
|
||||
|
||||
if (openingNoteIndex !== -1) {
|
||||
content = content.substr(openingNoteIndex + 9);
|
||||
}
|
||||
|
||||
const closingNoteIndex = content.lastIndexOf('</en-note>');
|
||||
const closingNoteIndex = content.lastIndexOf("</en-note>");
|
||||
|
||||
if (closingNoteIndex !== -1) {
|
||||
content = content.substr(0, closingNoteIndex);
|
||||
@@ -109,15 +106,20 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
|
||||
// Replace OneNote converted checkboxes with unicode ballot box based
|
||||
// on known hash of checkboxes for regular, p1, and p2 checkboxes
|
||||
content = content.replace(/<en-media alt="To Do( priority [12])?" hash="(74de5d3d1286f01bac98d32a09f601d9|4a19d3041585e11643e808d68dd3e72f|8e17580123099ac6515c3634b1f6f9a1)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g, "\u2610 ");
|
||||
content = content.replace(/<en-media alt="To Do( priority [12])?" hash="(5069b775461e471a47ce04ace6e1c6ae|7912ee9cec35fc3dba49edb63a9ed158|3a05f4f006a6eaf2627dae5ed8b8013b)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g, "\u2611 ");
|
||||
content = content.replace(
|
||||
/<en-media alt="To Do( priority [12])?" hash="(74de5d3d1286f01bac98d32a09f601d9|4a19d3041585e11643e808d68dd3e72f|8e17580123099ac6515c3634b1f6f9a1)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g,
|
||||
"\u2610 "
|
||||
);
|
||||
content = content.replace(
|
||||
/<en-media alt="To Do( priority [12])?" hash="(5069b775461e471a47ce04ace6e1c6ae|7912ee9cec35fc3dba49edb63a9ed158|3a05f4f006a6eaf2627dae5ed8b8013b)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g,
|
||||
"\u2611 "
|
||||
);
|
||||
|
||||
content = htmlSanitizer.sanitize(content);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
const path: string[] = [];
|
||||
|
||||
function getCurrentTag() {
|
||||
@@ -132,7 +134,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
}
|
||||
}
|
||||
|
||||
saxStream.on("error", e => {
|
||||
saxStream.on("error", (e) => {
|
||||
// unhandled errors will throw, since this is a proper node event emitter.
|
||||
log.error(`error when parsing ENEX file: ${e}`);
|
||||
// clear the error
|
||||
@@ -140,92 +142,86 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
saxStream._parser.resume();
|
||||
});
|
||||
|
||||
saxStream.on("text", text => {
|
||||
saxStream.on("text", (text) => {
|
||||
const currentTag = getCurrentTag();
|
||||
const previousTag = getPreviousTag();
|
||||
|
||||
if (previousTag === 'note-attributes') {
|
||||
if (previousTag === "note-attributes") {
|
||||
let labelName = currentTag;
|
||||
|
||||
if (labelName === 'source-url') {
|
||||
labelName = 'pageUrl';
|
||||
if (labelName === "source-url") {
|
||||
labelName = "pageUrl";
|
||||
}
|
||||
|
||||
labelName = sanitizeAttributeName(labelName || "");
|
||||
|
||||
if (note.attributes) {
|
||||
note.attributes.push({
|
||||
type: 'label',
|
||||
type: "label",
|
||||
name: labelName,
|
||||
value: text
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (previousTag === 'resource-attributes') {
|
||||
if (currentTag === 'file-name') {
|
||||
} else if (previousTag === "resource-attributes") {
|
||||
if (currentTag === "file-name") {
|
||||
resource.attributes.push({
|
||||
type: 'label',
|
||||
name: 'originalFileName',
|
||||
type: "label",
|
||||
name: "originalFileName",
|
||||
value: text
|
||||
});
|
||||
|
||||
resource.title = text;
|
||||
}
|
||||
else if (currentTag === 'source-url') {
|
||||
} else if (currentTag === "source-url") {
|
||||
resource.attributes.push({
|
||||
type: 'label',
|
||||
name: 'pageUrl',
|
||||
type: "label",
|
||||
name: "pageUrl",
|
||||
value: text
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (previousTag === 'resource') {
|
||||
if (currentTag === 'data') {
|
||||
text = text.replace(/\s/g, '');
|
||||
} else if (previousTag === "resource") {
|
||||
if (currentTag === "data") {
|
||||
text = text.replace(/\s/g, "");
|
||||
|
||||
// resource can be chunked into multiple events: https://github.com/zadam/trilium/issues/3424
|
||||
// it would probably make sense to do this in a more global way since it can in theory affect any field,
|
||||
// not just data
|
||||
resource.content = (resource.content || "") + text;
|
||||
}
|
||||
else if (currentTag === 'mime') {
|
||||
} else if (currentTag === "mime") {
|
||||
resource.mime = text.toLowerCase();
|
||||
}
|
||||
}
|
||||
else if (previousTag === 'note') {
|
||||
if (currentTag === 'title') {
|
||||
} else if (previousTag === "note") {
|
||||
if (currentTag === "title") {
|
||||
note.title = text;
|
||||
} else if (currentTag === 'created') {
|
||||
} else if (currentTag === "created") {
|
||||
note.utcDateCreated = parseDate(text);
|
||||
} else if (currentTag === 'updated') {
|
||||
} else if (currentTag === "updated") {
|
||||
note.utcDateModified = parseDate(text);
|
||||
} else if (currentTag === 'tag' && note.attributes) {
|
||||
} else if (currentTag === "tag" && note.attributes) {
|
||||
note.attributes.push({
|
||||
type: 'label',
|
||||
type: "label",
|
||||
name: sanitizeAttributeName(text),
|
||||
value: ''
|
||||
})
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
// unknown tags are just ignored
|
||||
}
|
||||
});
|
||||
|
||||
saxStream.on("attribute", attr => {
|
||||
saxStream.on("attribute", (attr) => {
|
||||
// an attribute. attr has "name" and "value"
|
||||
});
|
||||
|
||||
saxStream.on("opentag", tag => {
|
||||
saxStream.on("opentag", (tag) => {
|
||||
path.push(tag.name);
|
||||
|
||||
if (tag.name === 'note') {
|
||||
if (tag.name === "note") {
|
||||
note = {
|
||||
content: "",
|
||||
// it's an array, not a key-value object because we don't know if attributes can be duplicated
|
||||
attributes: [],
|
||||
resources: []
|
||||
};
|
||||
}
|
||||
else if (tag.name === 'resource') {
|
||||
} else if (tag.name === "resource") {
|
||||
resource = {
|
||||
title: "resource",
|
||||
attributes: []
|
||||
@@ -239,25 +235,29 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
|
||||
function updateDates(note: BNote, utcDateCreated?: string, utcDateModified?: string) {
|
||||
// it's difficult to force custom dateCreated and dateModified to Note entity, so we do it post-creation with SQL
|
||||
sql.execute(`
|
||||
sql.execute(
|
||||
`
|
||||
UPDATE notes
|
||||
SET dateCreated = ?,
|
||||
utcDateCreated = ?,
|
||||
dateModified = ?,
|
||||
utcDateModified = ?
|
||||
WHERE noteId = ?`,
|
||||
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, note.noteId]);
|
||||
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, note.noteId]
|
||||
);
|
||||
|
||||
sql.execute(`
|
||||
sql.execute(
|
||||
`
|
||||
UPDATE blobs
|
||||
SET utcDateModified = ?
|
||||
WHERE blobId = ?`,
|
||||
[utcDateModified, note.blobId]);
|
||||
[utcDateModified, note.blobId]
|
||||
);
|
||||
}
|
||||
|
||||
function saveNote() {
|
||||
// make a copy because stream continues with the next call and note gets overwritten
|
||||
let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note;
|
||||
let { title, content, attributes, resources, utcDateCreated, utcDateModified } = note;
|
||||
|
||||
if (!title || !content) {
|
||||
throw new Error("Missing title or content for note.");
|
||||
@@ -270,9 +270,9 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
title,
|
||||
content,
|
||||
utcDateCreated,
|
||||
type: 'text',
|
||||
mime: 'text/html',
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
}).note;
|
||||
|
||||
for (const attr of attributes || []) {
|
||||
@@ -297,16 +297,20 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
const hash = md5(resource.content);
|
||||
|
||||
// skip all checked/unchecked checkboxes from OneNote
|
||||
if (['74de5d3d1286f01bac98d32a09f601d9',
|
||||
'4a19d3041585e11643e808d68dd3e72f',
|
||||
'8e17580123099ac6515c3634b1f6f9a1',
|
||||
'5069b775461e471a47ce04ace6e1c6ae',
|
||||
'7912ee9cec35fc3dba49edb63a9ed158',
|
||||
'3a05f4f006a6eaf2627dae5ed8b8013b'].includes(hash)) {
|
||||
if (
|
||||
[
|
||||
"74de5d3d1286f01bac98d32a09f601d9",
|
||||
"4a19d3041585e11643e808d68dd3e72f",
|
||||
"8e17580123099ac6515c3634b1f6f9a1",
|
||||
"5069b775461e471a47ce04ace6e1c6ae",
|
||||
"7912ee9cec35fc3dba49edb63a9ed158",
|
||||
"3a05f4f006a6eaf2627dae5ed8b8013b"
|
||||
].includes(hash)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mediaRegex = new RegExp(`<en-media [^>]*hash="${hash}"[^>]*>`, 'g');
|
||||
const mediaRegex = new RegExp(`<en-media [^>]*hash="${hash}"[^>]*>`, "g");
|
||||
|
||||
resource.mime = resource.mime || "application/octet-stream";
|
||||
|
||||
@@ -319,9 +323,9 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
parentNoteId: noteEntity.noteId,
|
||||
title: resource.title,
|
||||
content: resource.content,
|
||||
type: 'file',
|
||||
type: "file",
|
||||
mime: resource.mime,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
}).note;
|
||||
|
||||
for (const attr of resource.attributes) {
|
||||
@@ -337,11 +341,9 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
content = (content || "").replace(mediaRegex, resourceLink);
|
||||
};
|
||||
|
||||
if (resource.mime && resource.mime.startsWith('image/')) {
|
||||
if (resource.mime && resource.mime.startsWith("image/")) {
|
||||
try {
|
||||
const originalName = (resource.title && resource.title !== 'resource')
|
||||
? resource.title
|
||||
: `image.${resource.mime.substr(6)}`; // default if real name is not present
|
||||
const originalName = resource.title && resource.title !== "resource" ? resource.title : `image.${resource.mime.substr(6)}`; // default if real name is not present
|
||||
|
||||
const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, !!taskContext.data?.shrinkImages);
|
||||
|
||||
@@ -375,10 +377,10 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
updateDates(noteEntity, utcDateCreated, utcDateModified);
|
||||
}
|
||||
|
||||
saxStream.on("closetag", tag => {
|
||||
saxStream.on("closetag", (tag) => {
|
||||
path.pop();
|
||||
|
||||
if (tag === 'note') {
|
||||
if (tag === "note") {
|
||||
saveNote();
|
||||
}
|
||||
});
|
||||
@@ -387,7 +389,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
//console.log("opencdata");
|
||||
});
|
||||
|
||||
saxStream.on("cdata", text => {
|
||||
saxStream.on("cdata", (text) => {
|
||||
note.content += text;
|
||||
});
|
||||
|
||||
@@ -395,8 +397,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
//console.log("closecdata");
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
// resolve only when we parse the whole document AND saving of all notes have been finished
|
||||
saxStream.on("end", () => resolve(rootNote));
|
||||
|
||||
@@ -405,7 +406,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
|
||||
bufferStream
|
||||
// rate limiting to improve responsiveness during / after import
|
||||
.pipe(new Throttle({rate: 500000}))
|
||||
.pipe(new Throttle({ rate: 500000 }))
|
||||
.pipe(saxStream);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,46 +2,46 @@
|
||||
|
||||
import mimeTypes from "mime-types";
|
||||
import path from "path";
|
||||
import { TaskData } from '../task_context_interface.js';
|
||||
import { TaskData } from "../task_context_interface.js";
|
||||
|
||||
const CODE_MIME_TYPES: Record<string, boolean | string> = {
|
||||
'text/plain': true,
|
||||
'text/x-csrc': true,
|
||||
'text/x-c++src': true,
|
||||
'text/x-csharp': true,
|
||||
'text/x-clojure': true,
|
||||
'text/css': true,
|
||||
'text/x-dockerfile': true,
|
||||
'text/x-erlang': true,
|
||||
'text/x-feature': true,
|
||||
'text/x-go': true,
|
||||
'text/x-groovy': true,
|
||||
'text/x-haskell': true,
|
||||
'text/html': true,
|
||||
'message/http': true,
|
||||
'text/x-java': true,
|
||||
'application/javascript': 'application/javascript;env=frontend',
|
||||
'application/x-javascript': 'application/javascript;env=frontend',
|
||||
'application/json': true,
|
||||
'text/x-kotlin': true,
|
||||
'text/x-stex': true,
|
||||
'text/x-lua': true,
|
||||
"text/plain": true,
|
||||
"text/x-csrc": true,
|
||||
"text/x-c++src": true,
|
||||
"text/x-csharp": true,
|
||||
"text/x-clojure": true,
|
||||
"text/css": true,
|
||||
"text/x-dockerfile": true,
|
||||
"text/x-erlang": true,
|
||||
"text/x-feature": true,
|
||||
"text/x-go": true,
|
||||
"text/x-groovy": true,
|
||||
"text/x-haskell": true,
|
||||
"text/html": true,
|
||||
"message/http": true,
|
||||
"text/x-java": true,
|
||||
"application/javascript": "application/javascript;env=frontend",
|
||||
"application/x-javascript": "application/javascript;env=frontend",
|
||||
"application/json": true,
|
||||
"text/x-kotlin": true,
|
||||
"text/x-stex": true,
|
||||
"text/x-lua": true,
|
||||
// possibly later migrate to text/markdown as primary MIME
|
||||
'text/markdown': 'text/x-markdown',
|
||||
'text/x-markdown': true,
|
||||
'text/x-objectivec': true,
|
||||
'text/x-pascal': true,
|
||||
'text/x-perl': true,
|
||||
'text/x-php': true,
|
||||
'text/x-python': true,
|
||||
'text/x-ruby': true,
|
||||
'text/x-rustsrc': true,
|
||||
'text/x-scala': true,
|
||||
'text/x-sh': true,
|
||||
'text/x-sql': true,
|
||||
'text/x-swift': true,
|
||||
'text/xml': true,
|
||||
'text/x-yaml': true
|
||||
"text/markdown": "text/x-markdown",
|
||||
"text/x-markdown": true,
|
||||
"text/x-objectivec": true,
|
||||
"text/x-pascal": true,
|
||||
"text/x-perl": true,
|
||||
"text/x-php": true,
|
||||
"text/x-python": true,
|
||||
"text/x-ruby": true,
|
||||
"text/x-rustsrc": true,
|
||||
"text/x-scala": true,
|
||||
"text/x-sh": true,
|
||||
"text/x-sql": true,
|
||||
"text/x-swift": true,
|
||||
"text/xml": true,
|
||||
"text/x-yaml": true
|
||||
};
|
||||
|
||||
// extensions missing in mime-db
|
||||
@@ -67,7 +67,7 @@ const EXTENSION_TO_MIME: Record<string, string> = {
|
||||
|
||||
/** @returns false if MIME is not detected */
|
||||
function getMime(fileName: string) {
|
||||
if (fileName.toLowerCase() === 'dockerfile') {
|
||||
if (fileName.toLowerCase() === "dockerfile") {
|
||||
return "text/x-dockerfile";
|
||||
}
|
||||
|
||||
@@ -81,24 +81,21 @@ function getMime(fileName: string) {
|
||||
}
|
||||
|
||||
function getType(options: TaskData, mime: string) {
|
||||
mime = mime ? mime.toLowerCase() : '';
|
||||
mime = mime ? mime.toLowerCase() : "";
|
||||
|
||||
if (options.textImportedAsText && (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime))) {
|
||||
return 'text';
|
||||
}
|
||||
else if (options.codeImportedAsCode && mime in CODE_MIME_TYPES) {
|
||||
return 'code';
|
||||
}
|
||||
else if (mime.startsWith("image/")) {
|
||||
return 'image';
|
||||
}
|
||||
else {
|
||||
return 'file';
|
||||
if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) {
|
||||
return "text";
|
||||
} else if (options.codeImportedAsCode && mime in CODE_MIME_TYPES) {
|
||||
return "code";
|
||||
} else if (mime.startsWith("image/")) {
|
||||
return "image";
|
||||
} else {
|
||||
return "file";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMimeType(mime: string) {
|
||||
mime = mime ? mime.toLowerCase() : '';
|
||||
mime = mime ? mime.toLowerCase() : "";
|
||||
const mappedMime = CODE_MIME_TYPES[mime];
|
||||
|
||||
if (mappedMime === true) {
|
||||
|
||||
@@ -14,9 +14,9 @@ interface OpmlXml {
|
||||
|
||||
interface OpmlBody {
|
||||
$: {
|
||||
version: string
|
||||
}
|
||||
body: OpmlOutline[]
|
||||
version: string;
|
||||
};
|
||||
body: OpmlOutline[];
|
||||
}
|
||||
|
||||
interface OpmlOutline {
|
||||
@@ -29,19 +29,17 @@ interface OpmlOutline {
|
||||
}
|
||||
|
||||
async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer, parentNote: BNote) {
|
||||
const xml = await new Promise<OpmlXml>(function(resolve, reject)
|
||||
{
|
||||
const xml = await new Promise<OpmlXml>(function (resolve, reject) {
|
||||
parseString(fileBuffer, function (err: any, result: OpmlXml) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!['1.0', '1.1', '2.0'].includes(xml.opml.$.version)) {
|
||||
if (!["1.0", "1.1", "2.0"].includes(xml.opml.$.version)) {
|
||||
return [400, `Unsupported OPML version ${xml.opml.$.version}, 1.0, 1.1 or 2.0 expected instead.`];
|
||||
}
|
||||
|
||||
@@ -57,30 +55,28 @@ async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer,
|
||||
if (!title || !title.trim()) {
|
||||
// https://github.com/zadam/trilium/issues/1862
|
||||
title = outline.$.text;
|
||||
content = '';
|
||||
content = "";
|
||||
}
|
||||
}
|
||||
else if (opmlVersion === 2) {
|
||||
} else if (opmlVersion === 2) {
|
||||
title = outline.$.text;
|
||||
content = outline.$._note; // _note is already HTML
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(`Unrecognized OPML version ${opmlVersion}`);
|
||||
}
|
||||
|
||||
content = htmlSanitizer.sanitize(content || "");
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title,
|
||||
content,
|
||||
type: 'text',
|
||||
type: "text",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
for (const childOutline of (outline.outline || [])) {
|
||||
for (const childOutline of outline.outline || []) {
|
||||
importOutline(childOutline, note.noteId);
|
||||
}
|
||||
|
||||
@@ -102,10 +98,10 @@ async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer,
|
||||
|
||||
function toHtml(text: string) {
|
||||
if (!text) {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
return `<p>${text.replace(/(?:\r\n|\r|\n)/g, '</p><p>')}</p>`;
|
||||
return `<p>${text.replace(/(?:\r\n|\r|\n)/g, "</p><p>")}</p>`;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -17,16 +17,16 @@ function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNot
|
||||
const mime = mimeService.getMime(file.originalname) || file.mimetype;
|
||||
|
||||
if (taskContext?.data?.textImportedAsText) {
|
||||
if (mime === 'text/html') {
|
||||
if (mime === "text/html") {
|
||||
return importHtml(taskContext, file, parentNote);
|
||||
} else if (['text/markdown', 'text/x-markdown'].includes(mime)) {
|
||||
} else if (["text/markdown", "text/x-markdown"].includes(mime)) {
|
||||
return importMarkdown(taskContext, file, parentNote);
|
||||
} else if (mime === 'text/plain') {
|
||||
} else if (mime === "text/plain") {
|
||||
return importPlainText(taskContext, file, parentNote);
|
||||
}
|
||||
}
|
||||
|
||||
if (taskContext?.data?.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === 'code') {
|
||||
if (taskContext?.data?.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === "code") {
|
||||
return importCodeNote(taskContext, file, parentNote);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext) {
|
||||
if (typeof file.buffer === "string") {
|
||||
throw new Error("Invalid file content for image.");
|
||||
}
|
||||
const {note} = imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, !!taskContext.data?.shrinkImages);
|
||||
const { note } = imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, !!taskContext.data?.shrinkImages);
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
@@ -51,12 +51,12 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext) {
|
||||
function importFile(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||
const originalName = file.originalname;
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title: originalName,
|
||||
content: file.buffer,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: 'file',
|
||||
type: "file",
|
||||
mime: mimeService.getMime(originalName) || file.mimetype
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote)
|
||||
parentNoteId: parentNote.noteId,
|
||||
title,
|
||||
content,
|
||||
type: 'code',
|
||||
type: "code",
|
||||
mime: mime,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
@@ -92,13 +92,13 @@ function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote
|
||||
const plainTextContent = file.buffer.toString("utf-8");
|
||||
const htmlContent = convertTextToHtml(plainTextContent);
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title,
|
||||
content: htmlContent,
|
||||
type: 'text',
|
||||
mime: 'text/html',
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
@@ -108,9 +108,7 @@ function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote
|
||||
|
||||
function convertTextToHtml(text: string) {
|
||||
// 1: Plain Text Search
|
||||
text = text.replace(/&/g, "&").
|
||||
replace(/</g, "<").
|
||||
replace(/>/g, ">");
|
||||
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
// 2: Line Breaks
|
||||
text = text.replace(/\r\n?|\n/g, "<br>");
|
||||
@@ -134,13 +132,13 @@ function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote)
|
||||
htmlContent = htmlSanitizer.sanitize(htmlContent);
|
||||
}
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title,
|
||||
content: htmlContent,
|
||||
type: 'text',
|
||||
mime: 'text/html',
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
@@ -162,14 +160,13 @@ function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||
content = htmlSanitizer.sanitize(content);
|
||||
}
|
||||
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title,
|
||||
content,
|
||||
type: 'text',
|
||||
mime: 'text/html',
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
@@ -188,7 +185,7 @@ function importAttachment(taskContext: TaskContext, file: File, parentNote: BNot
|
||||
parentNote.saveAttachment({
|
||||
title: file.originalname,
|
||||
content: file.buffer,
|
||||
role: 'file',
|
||||
role: "file",
|
||||
mime: mime
|
||||
});
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ import TaskContext from "../task_context.js";
|
||||
import BNote from "../../becca/entities/bnote.js";
|
||||
import NoteMeta from "../meta/note_meta.js";
|
||||
import AttributeMeta from "../meta/attribute_meta.js";
|
||||
import { Stream } from 'stream';
|
||||
import { ALLOWED_NOTE_TYPES, NoteType } from '../../becca/entities/rows.js';
|
||||
import { Stream } from "stream";
|
||||
import { ALLOWED_NOTE_TYPES, NoteType } from "../../becca/entities/rows.js";
|
||||
|
||||
interface MetaFile {
|
||||
files: NoteMeta[]
|
||||
files: NoteMeta[];
|
||||
}
|
||||
|
||||
async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRootNote: BNote): Promise<BNote> {
|
||||
@@ -34,7 +34,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
const attributes: AttributeMeta[] = [];
|
||||
// path => noteId, used only when meta file is not available
|
||||
/** path => noteId | attachmentId */
|
||||
const createdPaths: Record<string, string> = { '/': importRootNote.noteId, '\\': importRootNote.noteId };
|
||||
const createdPaths: Record<string, string> = { "/": importRootNote.noteId, "\\": importRootNote.noteId };
|
||||
let metaFile: MetaFile | null = null;
|
||||
let firstNote: BNote | null = null;
|
||||
const createdNoteIds = new Set<string>();
|
||||
@@ -45,7 +45,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
return "empty_note_id";
|
||||
}
|
||||
|
||||
if (origNoteId === 'root' || origNoteId.startsWith("_")) {
|
||||
if (origNoteId === "root" || origNoteId.startsWith("_")) {
|
||||
// these "named" noteIds don't differ between Trilium instances
|
||||
return origNoteId;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
|
||||
parent = cursor;
|
||||
if (parent.children) {
|
||||
cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
|
||||
cursor = parent.children.find((file) => file.dataFileName === segment || file.dirFileName === segment);
|
||||
}
|
||||
|
||||
if (!cursor) {
|
||||
@@ -128,11 +128,10 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
|
||||
if (parentNoteMeta?.noteId) {
|
||||
parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const parentPath = path.dirname(filePath);
|
||||
|
||||
if (parentPath === '.') {
|
||||
if (parentPath === ".") {
|
||||
parentNoteId = importRootNote.noteId;
|
||||
} else if (parentPath in createdPaths) {
|
||||
parentNoteId = createdPaths[parentPath];
|
||||
@@ -180,12 +179,11 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
for (const attr of noteMeta.attributes || []) {
|
||||
attr.noteId = note.noteId;
|
||||
|
||||
if (attr.type === 'label-definition') {
|
||||
attr.type = 'label';
|
||||
if (attr.type === "label-definition") {
|
||||
attr.type = "label";
|
||||
attr.name = `label:${attr.name}`;
|
||||
}
|
||||
else if (attr.type === 'relation-definition') {
|
||||
attr.type = 'label';
|
||||
} else if (attr.type === "relation-definition") {
|
||||
attr.type = "label";
|
||||
attr.name = `relation:${attr.name}`;
|
||||
}
|
||||
|
||||
@@ -194,12 +192,12 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attr.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(attr.name)) {
|
||||
if (attr.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(attr.name)) {
|
||||
// these relations are created automatically and as such don't need to be duplicated in the import
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attr.type === 'relation') {
|
||||
if (attr.type === "relation") {
|
||||
attr.value = getNewNoteId(attr.value);
|
||||
}
|
||||
|
||||
@@ -232,17 +230,17 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
throw new Error("Missing parent note ID.");
|
||||
}
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNoteId,
|
||||
title: noteTitle || "",
|
||||
content: '',
|
||||
content: "",
|
||||
noteId: noteId,
|
||||
type: resolveNoteType(noteMeta?.type),
|
||||
mime: noteMeta ? noteMeta.mime : 'text/html',
|
||||
prefix: noteMeta?.prefix || '',
|
||||
mime: noteMeta ? noteMeta.mime : "text/html",
|
||||
prefix: noteMeta?.prefix || "",
|
||||
isExpanded: !!noteMeta?.isExpanded,
|
||||
notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined,
|
||||
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
|
||||
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
createdNoteIds.add(note.noteId);
|
||||
@@ -267,11 +265,11 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
url = url.substr(3);
|
||||
}
|
||||
|
||||
if (absUrl === '.') {
|
||||
absUrl = '';
|
||||
if (absUrl === ".") {
|
||||
absUrl = "";
|
||||
}
|
||||
|
||||
absUrl += `${absUrl.length > 0 ? '/' : ''}${url}`;
|
||||
absUrl += `${absUrl.length > 0 ? "/" : ""}${url}`;
|
||||
|
||||
const { noteMeta, attachmentMeta } = getMeta(absUrl);
|
||||
|
||||
@@ -280,7 +278,8 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
||||
noteId: getNewNoteId(noteMeta.noteId)
|
||||
};
|
||||
} else { // don't check for noteMeta since it's not mandatory for notes
|
||||
} else {
|
||||
// don't check for noteMeta since it's not mandatory for notes
|
||||
return {
|
||||
noteId: getNoteId(noteMeta, absUrl)
|
||||
};
|
||||
@@ -345,8 +344,10 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
return `href="${url}"`;
|
||||
}
|
||||
|
||||
if (url.startsWith('#') // already a note path (probably)
|
||||
|| isUrlAbsolute(url)) {
|
||||
if (
|
||||
url.startsWith("#") || // already a note path (probably)
|
||||
isUrlAbsolute(url)
|
||||
) {
|
||||
return match;
|
||||
}
|
||||
|
||||
@@ -362,8 +363,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
});
|
||||
|
||||
if (noteMeta) {
|
||||
const includeNoteLinks = (noteMeta.attributes || [])
|
||||
.filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink');
|
||||
const includeNoteLinks = (noteMeta.attributes || []).filter((attr) => attr.type === "relation" && attr.name === "includeNoteLink");
|
||||
|
||||
for (const link of includeNoteLinks) {
|
||||
// no need to escape the regexp find string since it's a noteId which doesn't contain any special characters
|
||||
@@ -377,31 +377,25 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
}
|
||||
|
||||
function removeTriliumTags(content: string) {
|
||||
const tagsToRemove = [
|
||||
'<h1 data-trilium-h1>([^<]*)<\/h1>',
|
||||
'<title data-trilium-title>([^<]*)<\/title>'
|
||||
]
|
||||
const tagsToRemove = ["<h1 data-trilium-h1>([^<]*)<\/h1>", "<title data-trilium-title>([^<]*)<\/title>"];
|
||||
for (const tag of tagsToRemove) {
|
||||
let re = new RegExp(tag, "gi");
|
||||
content = content.replace(re, '');
|
||||
content = content.replace(re, "");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function processNoteContent(noteMeta: NoteMeta | undefined, type: string, mime: string, content: string | Buffer, noteTitle: string, filePath: string) {
|
||||
if ((noteMeta?.format === 'markdown'
|
||||
|| (!noteMeta && taskContext.data?.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime)))
|
||||
&& typeof content === "string") {
|
||||
if ((noteMeta?.format === "markdown" || (!noteMeta && taskContext.data?.textImportedAsText && ["text/markdown", "text/x-markdown"].includes(mime))) && typeof content === "string") {
|
||||
content = markdownService.renderToHtml(content, noteTitle);
|
||||
}
|
||||
|
||||
if (type === 'text' && typeof content === "string") {
|
||||
if (type === "text" && typeof content === "string") {
|
||||
content = processTextNoteContent(content, noteTitle, filePath, noteMeta);
|
||||
}
|
||||
|
||||
if (type === 'relationMap' && noteMeta && typeof content === "string") {
|
||||
const relationMapLinks = (noteMeta.attributes || [])
|
||||
.filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
|
||||
if (type === "relationMap" && noteMeta && typeof content === "string") {
|
||||
const relationMapLinks = (noteMeta.attributes || []).filter((attr) => attr.type === "relation" && attr.name === "relationMapLink");
|
||||
|
||||
// this will replace relation map links
|
||||
for (const link of relationMapLinks) {
|
||||
@@ -462,7 +456,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
throw new Error("Unable to resolve mime type.");
|
||||
}
|
||||
|
||||
if (type !== 'file' && type !== 'image') {
|
||||
if (type !== "file" && type !== "image") {
|
||||
content = content.toString("utf-8");
|
||||
}
|
||||
|
||||
@@ -496,21 +490,20 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
notePosition: noteMeta?.notePosition
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
else {
|
||||
({note} = noteService.createNewNote({
|
||||
} else {
|
||||
({ note } = noteService.createNewNote({
|
||||
parentNoteId: parentNoteId,
|
||||
title: noteTitle || "",
|
||||
content: content,
|
||||
noteId,
|
||||
type,
|
||||
mime,
|
||||
prefix: noteMeta?.prefix || '',
|
||||
prefix: noteMeta?.prefix || "",
|
||||
isExpanded: !!noteMeta?.isExpanded,
|
||||
// root notePosition should be ignored since it relates to the original document
|
||||
// now import root should be placed after existing notes into new parent
|
||||
notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined,
|
||||
isProtected: isProtected,
|
||||
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
|
||||
isProtected: isProtected
|
||||
}));
|
||||
|
||||
createdNoteIds.add(note.noteId);
|
||||
@@ -520,11 +513,11 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
firstNote = firstNote || note;
|
||||
}
|
||||
|
||||
if (!noteMeta && (type === 'file' || type === 'image')) {
|
||||
if (!noteMeta && (type === "file" || type === "image")) {
|
||||
attributes.push({
|
||||
noteId,
|
||||
type: 'label',
|
||||
name: 'originalFileName',
|
||||
type: "label",
|
||||
name: "originalFileName",
|
||||
value: path.basename(filePath)
|
||||
});
|
||||
}
|
||||
@@ -535,7 +528,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
|
||||
const filePath = normalizeFilePath(entry.fileName);
|
||||
|
||||
if (filePath === '!!!meta.json') {
|
||||
if (filePath === "!!!meta.json") {
|
||||
const content = await readContent(zipfile, entry);
|
||||
|
||||
metaFile = JSON.parse(content.toString("utf-8"));
|
||||
@@ -549,8 +542,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
saveDirectory(filePath);
|
||||
}
|
||||
else if (filePath !== '!!!meta.json') {
|
||||
} else if (filePath !== "!!!meta.json") {
|
||||
const content = await readContent(zipfile, entry);
|
||||
|
||||
saveNote(filePath, content);
|
||||
@@ -568,7 +560,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
if (!metaFile) {
|
||||
// if there's no meta file, then the notes are created based on the order in that zip file but that
|
||||
// is usually quite random, so we sort the notes in the way they would appear in the file manager
|
||||
treeService.sortNotes(noteId, 'title', false, true);
|
||||
treeService.sortNotes(noteId, "title", false, true);
|
||||
}
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
@@ -577,10 +569,9 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
// we're saving attributes and links only now so that all relation and link target notes
|
||||
// are already in the database (we don't want to have "broken" relations, not even transitionally)
|
||||
for (const attr of attributes) {
|
||||
if (attr.type !== 'relation' || attr.value in becca.notes) {
|
||||
if (attr.type !== "relation" || attr.value in becca.notes) {
|
||||
new BAttribute(attr).save();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
log.info(`Relation not imported since the target note doesn't exist: ${JSON.stringify(attr)}`);
|
||||
}
|
||||
}
|
||||
@@ -609,14 +600,14 @@ function normalizeFilePath(filePath: string): string {
|
||||
|
||||
function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||
const chunks: Uint8Array[] = [];
|
||||
stream.on('data', chunk => chunks.push(chunk));
|
||||
stream.on("data", (chunk) => chunks.push(chunk));
|
||||
|
||||
return new Promise((res, rej) => stream.on('end', () => res(Buffer.concat(chunks))));
|
||||
return new Promise((res, rej) => stream.on("end", () => res(Buffer.concat(chunks))));
|
||||
}
|
||||
|
||||
function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
|
||||
return new Promise((res, rej) => {
|
||||
zipfile.openReadStream(entry, function(err, readStream) {
|
||||
zipfile.openReadStream(entry, function (err, readStream) {
|
||||
if (err) rej(err);
|
||||
if (!readStream) throw new Error("Unable to read content.");
|
||||
|
||||
@@ -627,12 +618,12 @@ function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer
|
||||
|
||||
function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => void) {
|
||||
return new Promise((res, rej) => {
|
||||
yauzl.fromBuffer(buffer, {lazyEntries: true, validateEntrySizes: false}, function(err, zipfile) {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, function (err, zipfile) {
|
||||
if (err) rej(err);
|
||||
if (!zipfile) throw new Error("Unable to read zip file.");
|
||||
|
||||
zipfile.readEntry();
|
||||
zipfile.on("entry", async entry => {
|
||||
zipfile.on("entry", async (entry) => {
|
||||
try {
|
||||
await processEntryCallback(zipfile, entry);
|
||||
} catch (e) {
|
||||
@@ -646,12 +637,12 @@ function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFi
|
||||
|
||||
function resolveNoteType(type: string | undefined): NoteType {
|
||||
// BC for ZIPs created in Triliun 0.57 and older
|
||||
if (type === 'relation-map') {
|
||||
return 'relationMap';
|
||||
} else if (type === 'note-map') {
|
||||
return 'noteMap';
|
||||
} else if (type === 'web-view') {
|
||||
return 'webView';
|
||||
if (type === "relation-map") {
|
||||
return "relationMap";
|
||||
} else if (type === "note-map") {
|
||||
return "noteMap";
|
||||
} else if (type === "web-view") {
|
||||
return "webView";
|
||||
}
|
||||
|
||||
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import optionService from "./options.js";
|
||||
import log from "./log.js";
|
||||
import { isElectron as getIsElectron, isMac as getIsMac } from "./utils.js";
|
||||
import { KeyboardShortcut } from './keyboard_actions_interface.js';
|
||||
import { KeyboardShortcut } from "./keyboard_actions_interface.js";
|
||||
import { t } from "i18next";
|
||||
|
||||
const isMac = getIsMac();
|
||||
@@ -77,7 +77,6 @@ function getDefaultKeyboardActions() {
|
||||
scope: "note-tree"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
separator: t("keyboard_actions.creating-and-moving-notes")
|
||||
},
|
||||
@@ -197,7 +196,6 @@ function getDefaultKeyboardActions() {
|
||||
scope: "note-tree"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
separator: t("keyboard_actions.tabs-and-windows")
|
||||
},
|
||||
@@ -304,7 +302,6 @@ function getDefaultKeyboardActions() {
|
||||
scope: "window"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
separator: t("keyboard_actions.dialogs")
|
||||
},
|
||||
@@ -351,7 +348,6 @@ function getDefaultKeyboardActions() {
|
||||
scope: "window"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
separator: t("keyboard_actions.text-note-operations")
|
||||
},
|
||||
@@ -602,13 +598,13 @@ function getDefaultKeyboardActions() {
|
||||
];
|
||||
|
||||
/*
|
||||
* Apply macOS-specific tweaks.
|
||||
*/
|
||||
const platformModifier = isMac ? 'Meta' : 'Ctrl';
|
||||
* Apply macOS-specific tweaks.
|
||||
*/
|
||||
const platformModifier = isMac ? "Meta" : "Ctrl";
|
||||
|
||||
for (const action of DEFAULT_KEYBOARD_ACTIONS) {
|
||||
if (action.defaultShortcuts) {
|
||||
action.defaultShortcuts = action.defaultShortcuts.map(shortcut => shortcut.replace("CommandOrControl", platformModifier));
|
||||
action.defaultShortcuts = action.defaultShortcuts.map((shortcut) => shortcut.replace("CommandOrControl", platformModifier));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,21 +619,19 @@ function getKeyboardActions() {
|
||||
}
|
||||
|
||||
for (const option of optionService.getOptions()) {
|
||||
if (option.name.startsWith('keyboardShortcuts')) {
|
||||
if (option.name.startsWith("keyboardShortcuts")) {
|
||||
let actionName = option.name.substring(17);
|
||||
actionName = actionName.charAt(0).toLowerCase() + actionName.slice(1);
|
||||
|
||||
const action = actions.find(ea => ea.actionName === actionName);
|
||||
const action = actions.find((ea) => ea.actionName === actionName);
|
||||
|
||||
if (action) {
|
||||
try {
|
||||
action.effectiveShortcuts = JSON.parse(option.value);
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
log.error(`Could not parse shortcuts for action ${actionName}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
log.info(`Keyboard action ${actionName} found in database, but not in action definition.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,13 +101,13 @@ export interface KeyboardShortcut {
|
||||
defaultShortcuts?: string[];
|
||||
effectiveShortcuts?: string[];
|
||||
/**
|
||||
* Scope here means on which element the keyboard shortcuts are attached - this means that for the shortcut to work,
|
||||
* the focus has to be inside the element.
|
||||
*
|
||||
* So e.g. shortcuts with "note-tree" scope work only when the focus is in note tree.
|
||||
* This allows to have the same shortcut have different actions attached based on the context
|
||||
* e.g. CTRL-C in note tree does something a bit different from CTRL-C in the text editor.
|
||||
*/
|
||||
* Scope here means on which element the keyboard shortcuts are attached - this means that for the shortcut to work,
|
||||
* the focus has to be inside the element.
|
||||
*
|
||||
* So e.g. shortcuts with "note-tree" scope work only when the focus is in note tree.
|
||||
* This allows to have the same shortcut have different actions attached based on the context
|
||||
* e.g. CTRL-C in note tree does something a bit different from CTRL-C in the text editor.
|
||||
*/
|
||||
scope?: "window" | "note-tree" | "text-detail" | "code-detail";
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
const NEW_LINE = isWindows() ? '\r\n' : '\n';
|
||||
const NEW_LINE = isWindows() ? "\r\n" : "\n";
|
||||
|
||||
let todaysMidnight!: Date;
|
||||
|
||||
@@ -38,7 +38,7 @@ function initLogFile() {
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
logFile = fs.createWriteStream(path, {flags: 'a'});
|
||||
logFile = fs.createWriteStream(path, { flags: "a" });
|
||||
}
|
||||
|
||||
function checkDate(millisSinceMidnight: number) {
|
||||
@@ -75,7 +75,7 @@ function error(message: string | Error) {
|
||||
log(`ERROR: ${message}`);
|
||||
}
|
||||
|
||||
const requestBlacklist = [ "/libraries", "/app", "/images", "/stylesheets", "/api/recent-notes" ];
|
||||
const requestBlacklist = ["/libraries", "/app", "/images", "/stylesheets", "/api/recent-notes"];
|
||||
|
||||
function request(req: Request, res: Response, timeMs: number, responseLength: number | string = "?") {
|
||||
for (const bl of requestBlacklist) {
|
||||
@@ -88,24 +88,21 @@ function request(req: Request, res: Response, timeMs: number, responseLength: nu
|
||||
return;
|
||||
}
|
||||
|
||||
info((timeMs >= 10 ? "Slow " : "") +
|
||||
`${res.statusCode} ${req.method} ${req.url} with ${responseLength} bytes took ${timeMs}ms`);
|
||||
info((timeMs >= 10 ? "Slow " : "") + `${res.statusCode} ${req.method} ${req.url} with ${responseLength} bytes took ${timeMs}ms`);
|
||||
}
|
||||
|
||||
function pad(num: number) {
|
||||
num = Math.floor(num);
|
||||
|
||||
return num < 10 ? (`0${num}`) : num.toString();
|
||||
return num < 10 ? `0${num}` : num.toString();
|
||||
}
|
||||
|
||||
function padMilli(num: number) {
|
||||
if (num < 10) {
|
||||
return `00${num}`;
|
||||
}
|
||||
else if (num < 100) {
|
||||
} else if (num < 100) {
|
||||
return `0${num}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,7 @@ async function migrate() {
|
||||
// backup before attempting migration
|
||||
await backupService.backupNow(
|
||||
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
|
||||
currentDbVersion === 214
|
||||
? `before-migration-v060`
|
||||
: 'before-migration'
|
||||
currentDbVersion === 214 ? `before-migration-v060` : "before-migration"
|
||||
);
|
||||
|
||||
const migrationFiles = fs.readdirSync(resourceDir.MIGRATIONS_DIR);
|
||||
@@ -37,27 +35,29 @@ async function migrate() {
|
||||
return;
|
||||
}
|
||||
|
||||
const migrations = migrationFiles.map(file => {
|
||||
const match = file.match(/^([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const migrations = migrationFiles
|
||||
.map((file) => {
|
||||
const match = file.match(/^([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dbVersion = parseInt(match[1]);
|
||||
if (dbVersion > currentDbVersion) {
|
||||
const name = match[2];
|
||||
const type = match[3];
|
||||
const dbVersion = parseInt(match[1]);
|
||||
if (dbVersion > currentDbVersion) {
|
||||
const name = match[2];
|
||||
const type = match[3];
|
||||
|
||||
return {
|
||||
dbVersion: dbVersion,
|
||||
name: name,
|
||||
file: file,
|
||||
type: type
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).filter((el): el is MigrationInfo => !!el);
|
||||
return {
|
||||
dbVersion: dbVersion,
|
||||
name: name,
|
||||
file: file,
|
||||
type: type
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((el): el is MigrationInfo => !!el);
|
||||
|
||||
migrations.sort((a, b) => a.dbVersion - b.dbVersion);
|
||||
|
||||
@@ -74,9 +74,12 @@ async function migrate() {
|
||||
|
||||
await executeMigration(mig);
|
||||
|
||||
sql.execute(`UPDATE options
|
||||
sql.execute(
|
||||
`UPDATE options
|
||||
SET value = ?
|
||||
WHERE name = ?`, [mig.dbVersion.toString(), "dbVersion"]);
|
||||
WHERE name = ?`,
|
||||
[mig.dbVersion.toString(), "dbVersion"]
|
||||
);
|
||||
|
||||
log.info(`Migration to version ${mig.dbVersion} has been successful.`);
|
||||
} catch (e: any) {
|
||||
@@ -97,13 +100,13 @@ async function migrate() {
|
||||
}
|
||||
|
||||
async function executeMigration(mig: MigrationInfo) {
|
||||
if (mig.type === 'sql') {
|
||||
const migrationSql = fs.readFileSync(`${resourceDir.MIGRATIONS_DIR}/${mig.file}`).toString('utf8');
|
||||
if (mig.type === "sql") {
|
||||
const migrationSql = fs.readFileSync(`${resourceDir.MIGRATIONS_DIR}/${mig.file}`).toString("utf8");
|
||||
|
||||
console.log(`Migration with SQL script: ${migrationSql}`);
|
||||
|
||||
sql.executeScript(migrationSql);
|
||||
} else if (mig.type === 'js') {
|
||||
} else if (mig.type === "js") {
|
||||
console.log("Migration with JS module");
|
||||
|
||||
const migrationModule = await import(`${resourceDir.MIGRATIONS_DIR}/${mig.file}`);
|
||||
@@ -132,8 +135,10 @@ function isDbUpToDate() {
|
||||
async function migrateIfNecessary() {
|
||||
const currentDbVersion = getDbVersion();
|
||||
|
||||
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== 'true') {
|
||||
log.error(`Current DB version ${currentDbVersion} is newer than the current DB version ${appInfo.dbVersion}, which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue.`);
|
||||
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== "true") {
|
||||
log.error(
|
||||
`Current DB version ${currentDbVersion} is newer than the current DB version ${appInfo.dbVersion}, which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue.`
|
||||
);
|
||||
|
||||
await crash();
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
const noteTypes = [
|
||||
{ type: 'text', defaultMime: 'text/html' },
|
||||
{ type: 'code', defaultMime: 'text/plain' },
|
||||
{ type: 'render', defaultMime: '' },
|
||||
{ type: 'file', defaultMime: 'application/octet-stream' },
|
||||
{ type: 'image', defaultMime: '' },
|
||||
{ type: 'search', defaultMime: '' },
|
||||
{ type: 'relationMap', defaultMime: 'application/json' },
|
||||
{ type: 'book', defaultMime: '' },
|
||||
{ type: 'noteMap', defaultMime: '' },
|
||||
{ type: 'mermaid', defaultMime: 'text/plain' },
|
||||
{ type: 'canvas', defaultMime: 'application/json' },
|
||||
{ type: 'webView', defaultMime: '' },
|
||||
{ type: 'launcher', defaultMime: '' },
|
||||
{ type: 'doc', defaultMime: '' },
|
||||
{ type: 'contentWidget', defaultMime: '' },
|
||||
{ type: 'mindMap', defaultMime: 'application/json' }
|
||||
{ type: "text", defaultMime: "text/html" },
|
||||
{ type: "code", defaultMime: "text/plain" },
|
||||
{ type: "render", defaultMime: "" },
|
||||
{ type: "file", defaultMime: "application/octet-stream" },
|
||||
{ type: "image", defaultMime: "" },
|
||||
{ type: "search", defaultMime: "" },
|
||||
{ type: "relationMap", defaultMime: "application/json" },
|
||||
{ type: "book", defaultMime: "" },
|
||||
{ type: "noteMap", defaultMime: "" },
|
||||
{ type: "mermaid", defaultMime: "text/plain" },
|
||||
{ type: "canvas", defaultMime: "application/json" },
|
||||
{ type: "webView", defaultMime: "" },
|
||||
{ type: "launcher", defaultMime: "" },
|
||||
{ type: "doc", defaultMime: "" },
|
||||
{ type: "contentWidget", defaultMime: "" },
|
||||
{ type: "mindMap", defaultMime: "application/json" }
|
||||
];
|
||||
|
||||
function getDefaultMimeForNoteType(typeName: string) {
|
||||
const typeRec = noteTypes.find(nt => nt.type === typeName);
|
||||
const typeRec = noteTypes.find((nt) => nt.type === typeName);
|
||||
|
||||
if (!typeRec) {
|
||||
throw new Error(`Cannot find note type '${typeName}'`);
|
||||
@@ -28,6 +28,6 @@ function getDefaultMimeForNoteType(typeName: string) {
|
||||
}
|
||||
|
||||
export default {
|
||||
getNoteTypeNames: () => noteTypes.map(nt => nt.type),
|
||||
getNoteTypeNames: () => noteTypes.map((nt) => nt.type),
|
||||
getDefaultMimeForNoteType
|
||||
};
|
||||
|
||||
@@ -23,15 +23,15 @@ import noteTypesService from "./note_types.js";
|
||||
import fs from "fs";
|
||||
import ws from "./ws.js";
|
||||
import html2plaintext from "html2plaintext";
|
||||
import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from '../becca/entities/rows.js';
|
||||
import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from "../becca/entities/rows.js";
|
||||
import TaskContext from "./task_context.js";
|
||||
import { NoteParams } from './note-interface.js';
|
||||
import { NoteParams } from "./note-interface.js";
|
||||
import imageService from "./image.js";
|
||||
import { t } from "i18next";
|
||||
|
||||
interface FoundLink {
|
||||
name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink",
|
||||
value: string
|
||||
name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink";
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
@@ -40,15 +40,17 @@ interface Attachment {
|
||||
}
|
||||
|
||||
function getNewNotePosition(parentNote: BNote) {
|
||||
if (parentNote.isLabelTruthy('newNotesOnTop')) {
|
||||
const minNotePos = parentNote.getChildBranches()
|
||||
.filter(branch => branch?.noteId !== '_hidden') // has "always last" note position
|
||||
if (parentNote.isLabelTruthy("newNotesOnTop")) {
|
||||
const minNotePos = parentNote
|
||||
.getChildBranches()
|
||||
.filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position
|
||||
.reduce((min, note) => Math.min(min, note?.notePosition || 0), 0);
|
||||
|
||||
return minNotePos - 10;
|
||||
} else {
|
||||
const maxNotePos = parentNote.getChildBranches()
|
||||
.filter(branch => branch?.noteId !== '_hidden') // has "always last" note position
|
||||
const maxNotePos = parentNote
|
||||
.getChildBranches()
|
||||
.filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position
|
||||
.reduce((max, note) => Math.max(max, note?.notePosition || 0), 0);
|
||||
|
||||
return maxNotePos + 10;
|
||||
@@ -75,9 +77,9 @@ function copyChildAttributes(parentNote: BNote, childNote: BNote) {
|
||||
for (const attr of parentNote.getAttributes()) {
|
||||
if (attr.name.startsWith("child:")) {
|
||||
const name = attr.name.substr(6);
|
||||
const hasAlreadyTemplate = childNote.hasRelation('template');
|
||||
const hasAlreadyTemplate = childNote.hasRelation("template");
|
||||
|
||||
if (hasAlreadyTemplate && attr.type === 'relation' && name === 'template') {
|
||||
if (hasAlreadyTemplate && attr.type === "relation" && name === "template") {
|
||||
// if the note already has a template, it means the template was chosen by the user explicitly
|
||||
// in the menu. In that case, we should override the default templates defined in the child: attrs
|
||||
continue;
|
||||
@@ -98,7 +100,7 @@ function copyChildAttributes(parentNote: BNote, childNote: BNote) {
|
||||
function getNewNoteTitle(parentNote: BNote) {
|
||||
let title = t("notes.new-note");
|
||||
|
||||
const titleTemplate = parentNote.getLabelValue('titleTemplate');
|
||||
const titleTemplate = parentNote.getLabelValue("titleTemplate");
|
||||
|
||||
if (titleTemplate !== null) {
|
||||
try {
|
||||
@@ -135,19 +137,16 @@ function getAndValidateParent(params: GetValidateParams) {
|
||||
throw new ValidationError(`Parent note '${params.parentNoteId}' was not found.`);
|
||||
}
|
||||
|
||||
if (parentNote.type === 'launcher' && parentNote.noteId !== '_lbBookmarks') {
|
||||
if (parentNote.type === "launcher" && parentNote.noteId !== "_lbBookmarks") {
|
||||
throw new ValidationError(`Creating child notes into launcher notes is not allowed.`);
|
||||
}
|
||||
|
||||
if (['_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(params.parentNoteId) && params.type !== 'launcher') {
|
||||
if (["_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(params.parentNoteId) && params.type !== "launcher") {
|
||||
throw new ValidationError(`Only 'launcher' notes can be created in parent '${params.parentNoteId}'`);
|
||||
}
|
||||
|
||||
if (!params.ignoreForbiddenParents) {
|
||||
if (['_lbRoot', '_hidden'].includes(parentNote.noteId)
|
||||
|| parentNote.noteId.startsWith("_lbTpl")
|
||||
|| parentNote.isOptions()) {
|
||||
|
||||
if (["_lbRoot", "_hidden"].includes(parentNote.noteId) || parentNote.noteId.startsWith("_lbTpl") || parentNote.isOptions()) {
|
||||
throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`);
|
||||
}
|
||||
}
|
||||
@@ -170,11 +169,11 @@ function createNewNote(params: NoteParams): {
|
||||
}
|
||||
|
||||
let error;
|
||||
if (error = dateUtils.validateLocalDateTime(params.dateCreated)) {
|
||||
if ((error = dateUtils.validateLocalDateTime(params.dateCreated))) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (error = dateUtils.validateUtcDateTime(params.utcDateCreated)) {
|
||||
if ((error = dateUtils.validateUtcDateTime(params.utcDateCreated))) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
@@ -212,8 +211,7 @@ function createNewNote(params: NoteParams): {
|
||||
prefix: params.prefix || "",
|
||||
isExpanded: !!params.isExpanded
|
||||
}).save();
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
if (!isEntityEventsDisabled) {
|
||||
// re-enable entity events only if they were previously enabled
|
||||
// (they can be disabled in case of import)
|
||||
@@ -228,20 +226,20 @@ function createNewNote(params: NoteParams): {
|
||||
throw new Error(`Template note '${params.templateNoteId}' does not exist.`);
|
||||
}
|
||||
|
||||
note.addRelation('template', params.templateNoteId);
|
||||
note.addRelation("template", params.templateNoteId);
|
||||
|
||||
// no special handling for ~inherit since it doesn't matter if it's assigned with the note creation or later
|
||||
}
|
||||
|
||||
copyChildAttributes(parentNote, note);
|
||||
|
||||
eventService.emit(eventService.ENTITY_CREATED, { entityName: 'notes', entity: note });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'notes', entity: note });
|
||||
eventService.emit(eventService.ENTITY_CREATED, { entityName: "notes", entity: note });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "notes", entity: note });
|
||||
triggerNoteTitleChanged(note);
|
||||
// blobs entity doesn't use "created" event
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'blobs', entity: note });
|
||||
eventService.emit(eventService.ENTITY_CREATED, { entityName: 'branches', entity: branch });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'branches', entity: branch });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "blobs", entity: note });
|
||||
eventService.emit(eventService.ENTITY_CREATED, { entityName: "branches", entity: branch });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "branches", entity: branch });
|
||||
eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote: parentNote });
|
||||
|
||||
log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`);
|
||||
@@ -253,24 +251,22 @@ function createNewNote(params: NoteParams): {
|
||||
});
|
||||
}
|
||||
|
||||
function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: string | undefined, params: NoteParams) {
|
||||
function createNewNoteWithTarget(target: "into" | "after", targetBranchId: string | undefined, params: NoteParams) {
|
||||
if (!params.type) {
|
||||
const parentNote = becca.notes[params.parentNoteId];
|
||||
|
||||
// code note type can be inherited, otherwise "text" is the default
|
||||
params.type = parentNote.type === 'code' ? 'code' : 'text';
|
||||
params.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
|
||||
params.type = parentNote.type === "code" ? "code" : "text";
|
||||
params.mime = parentNote.type === "code" ? parentNote.mime : "text/html";
|
||||
}
|
||||
|
||||
if (target === 'into') {
|
||||
if (target === "into") {
|
||||
return createNewNote(params);
|
||||
}
|
||||
else if (target === 'after' && targetBranchId) {
|
||||
} else if (target === "after" && targetBranchId) {
|
||||
const afterBranch = becca.branches[targetBranchId];
|
||||
|
||||
// not updating utcDateModified to avoid having to sync whole rows
|
||||
sql.execute('UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0',
|
||||
[params.parentNoteId, afterBranch.notePosition]);
|
||||
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [params.parentNoteId, afterBranch.notePosition]);
|
||||
|
||||
params.notePosition = afterBranch.notePosition + 10;
|
||||
|
||||
@@ -279,8 +275,7 @@ function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: str
|
||||
entityChangesService.putNoteReorderingEntityChange(params.parentNoteId);
|
||||
|
||||
return retObject;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(`Unknown target '${target}'`);
|
||||
}
|
||||
}
|
||||
@@ -318,17 +313,15 @@ function protectNote(note: BNote, protect: boolean) {
|
||||
const content = attachment.getContent();
|
||||
|
||||
attachment.isProtected = protect;
|
||||
attachment.setContent(content, {forceSave: true});
|
||||
}
|
||||
catch (e) {
|
||||
attachment.setContent(content, { forceSave: true });
|
||||
} catch (e) {
|
||||
log.error(`Could not un/protect attachment '${attachment.attachmentId}'`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
log.error(`Could not un/protect note '${note.noteId}'`);
|
||||
|
||||
throw e;
|
||||
@@ -340,12 +333,12 @@ function checkImageAttachments(note: BNote, content: string) {
|
||||
let match;
|
||||
|
||||
const imgRegExp = /src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image/g;
|
||||
while (match = imgRegExp.exec(content)) {
|
||||
while ((match = imgRegExp.exec(content))) {
|
||||
foundAttachmentIds.add(match[1]);
|
||||
}
|
||||
|
||||
const linkRegExp = /href="[^"]+attachmentId=([a-zA-Z0-9_]+)/g;
|
||||
while (match = linkRegExp.exec(content)) {
|
||||
while ((match = linkRegExp.exec(content))) {
|
||||
foundAttachmentIds.add(match[1]);
|
||||
}
|
||||
|
||||
@@ -363,14 +356,14 @@ function checkImageAttachments(note: BNote, content: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const existingAttachmentIds = new Set<string | undefined>(attachments.map(att => att.attachmentId));
|
||||
const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId));
|
||||
const existingAttachmentIds = new Set<string | undefined>(attachments.map((att) => att.attachmentId));
|
||||
const unknownAttachmentIds = Array.from(foundAttachmentIds).filter((foundAttId) => !existingAttachmentIds.has(foundAttId));
|
||||
const unknownAttachments = becca.getAttachments(unknownAttachmentIds);
|
||||
|
||||
for (const unknownAttachment of unknownAttachments) {
|
||||
// the attachment belongs to a different note (was copy-pasted). Attachments can be linked only from the note
|
||||
// which owns it, so either find an existing attachment having the same content or make a copy.
|
||||
let localAttachment = note.getAttachments().find(att => att.role === unknownAttachment.role && att.blobId === unknownAttachment.blobId);
|
||||
let localAttachment = note.getAttachments().find((att) => att.role === unknownAttachment.role && att.blobId === unknownAttachment.blobId);
|
||||
|
||||
if (localAttachment) {
|
||||
if (localAttachment.utcDateScheduledForErasureSince) {
|
||||
@@ -379,21 +372,25 @@ function checkImageAttachments(note: BNote, content: string) {
|
||||
localAttachment.save();
|
||||
}
|
||||
|
||||
log.info(`Found equivalent attachment '${localAttachment.attachmentId}' of note '${note.noteId}' for the linked foreign attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.ownerId}'`);
|
||||
log.info(
|
||||
`Found equivalent attachment '${localAttachment.attachmentId}' of note '${note.noteId}' for the linked foreign attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.ownerId}'`
|
||||
);
|
||||
} else {
|
||||
localAttachment = unknownAttachment.copy();
|
||||
localAttachment.ownerId = note.noteId;
|
||||
localAttachment.setContent(unknownAttachment.getContent(), {forceSave: true});
|
||||
localAttachment.setContent(unknownAttachment.getContent(), { forceSave: true });
|
||||
|
||||
ws.sendMessageToAllClients({ type: 'toast', message: `Attachment '${localAttachment.title}' has been copied to note '${note.title}'.`});
|
||||
ws.sendMessageToAllClients({ type: "toast", message: `Attachment '${localAttachment.title}' has been copied to note '${note.title}'.` });
|
||||
log.info(`Copied attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.ownerId}' to new '${localAttachment.attachmentId}' of note '${note.noteId}'`);
|
||||
}
|
||||
|
||||
// replace image links
|
||||
content = content.replace(`api/attachments/${unknownAttachment.attachmentId}/image`, `api/attachments/${localAttachment.attachmentId}/image`);
|
||||
// replace reference links
|
||||
content = content.replace(new RegExp(`href="[^"]+attachmentId=${unknownAttachment.attachmentId}[^"]*"`, "g"),
|
||||
`href="#root/${localAttachment.ownerId}?viewMode=attachments&attachmentId=${localAttachment.attachmentId}"`);
|
||||
content = content.replace(
|
||||
new RegExp(`href="[^"]+attachmentId=${unknownAttachment.attachmentId}[^"]*"`, "g"),
|
||||
`href="#root/${localAttachment.ownerId}?viewMode=attachments&attachmentId=${localAttachment.attachmentId}"`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -406,9 +403,9 @@ function findImageLinks(content: string, foundLinks: FoundLink[]) {
|
||||
const re = /src="[^"]*api\/images\/([a-zA-Z0-9_]+)\//g;
|
||||
let match;
|
||||
|
||||
while (match = re.exec(content)) {
|
||||
while ((match = re.exec(content))) {
|
||||
foundLinks.push({
|
||||
name: 'imageLink',
|
||||
name: "imageLink",
|
||||
value: match[1]
|
||||
});
|
||||
}
|
||||
@@ -422,9 +419,9 @@ function findInternalLinks(content: string, foundLinks: FoundLink[]) {
|
||||
const re = /href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)\/?"/g;
|
||||
let match;
|
||||
|
||||
while (match = re.exec(content)) {
|
||||
while ((match = re.exec(content))) {
|
||||
foundLinks.push({
|
||||
name: 'internalLink',
|
||||
name: "internalLink",
|
||||
value: match[1]
|
||||
});
|
||||
}
|
||||
@@ -437,9 +434,9 @@ function findIncludeNoteLinks(content: string, foundLinks: FoundLink[]) {
|
||||
const re = /<section class="include-note[^>]+data-note-id="([a-zA-Z0-9_]+)"[^>]*>/g;
|
||||
let match;
|
||||
|
||||
while (match = re.exec(content)) {
|
||||
while ((match = re.exec(content))) {
|
||||
foundLinks.push({
|
||||
name: 'includeNoteLink',
|
||||
name: "includeNoteLink",
|
||||
value: match[1]
|
||||
});
|
||||
}
|
||||
@@ -453,7 +450,7 @@ function findRelationMapLinks(content: string, foundLinks: FoundLink[]) {
|
||||
|
||||
for (const note of obj.notes) {
|
||||
foundLinks.push({
|
||||
name: 'relationMapLink',
|
||||
name: "relationMapLink",
|
||||
value: note.noteId
|
||||
});
|
||||
}
|
||||
@@ -498,8 +495,7 @@ async function downloadImage(noteId: string, imageUrl: string) {
|
||||
}
|
||||
|
||||
log.info(`Download of '${imageUrl}' succeeded and was saved as image attachment '${attachment.attachmentId}' of note '${noteId}'`);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
log.error(`Download of '${imageUrl}' for note '${noteId}' failed with error: ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
@@ -514,27 +510,28 @@ function replaceUrl(content: string, url: string, attachment: Attachment) {
|
||||
}
|
||||
|
||||
function downloadImages(noteId: string, content: string) {
|
||||
const imageRe = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig;
|
||||
const imageRe = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/gi;
|
||||
let imageMatch;
|
||||
|
||||
while (imageMatch = imageRe.exec(content)) {
|
||||
while ((imageMatch = imageRe.exec(content))) {
|
||||
const url = imageMatch[1];
|
||||
const inlineImageMatch = /^data:image\/[a-z]+;base64,/.exec(url);
|
||||
|
||||
if (inlineImageMatch) {
|
||||
const imageBase64 = url.substr(inlineImageMatch[0].length);
|
||||
const imageBuffer = Buffer.from(imageBase64, 'base64');
|
||||
const imageBuffer = Buffer.from(imageBase64, "base64");
|
||||
|
||||
const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, "inline image", true, true);
|
||||
|
||||
const encodedTitle = encodeURIComponent(attachment.title);
|
||||
|
||||
content = `${content.substr(0, imageMatch.index)}<img src="api/attachments/${attachment.attachmentId}/image/${encodedTitle}"${content.substr(imageMatch.index + imageMatch[0].length)}`;
|
||||
}
|
||||
else if (!url.includes('api/images/') && !/api\/attachments\/.+\/image\/?.*/.test(url)
|
||||
} else if (
|
||||
!url.includes("api/images/") &&
|
||||
!/api\/attachments\/.+\/image\/?.*/.test(url) &&
|
||||
// this is an exception for the web clipper's "imageId"
|
||||
&& (url.length !== 20 || url.toLowerCase().startsWith('http'))) {
|
||||
|
||||
(url.length !== 20 || url.toLowerCase().startsWith("http"))
|
||||
) {
|
||||
if (!optionService.getOptionBool("downloadImagesAutomatically")) {
|
||||
continue;
|
||||
}
|
||||
@@ -544,8 +541,7 @@ function downloadImages(noteId: string, content: string) {
|
||||
|
||||
if (!attachment) {
|
||||
delete imageUrlToAttachmentIdMapping[url];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
content = replaceUrl(content, url, attachment);
|
||||
continue;
|
||||
}
|
||||
@@ -591,7 +587,7 @@ function downloadImages(noteId: string, content: string) {
|
||||
}
|
||||
|
||||
for (const url in imageUrlToAttachmentIdMapping) {
|
||||
const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]);
|
||||
const imageNote = imageNotes.find((note) => note.noteId === imageUrlToAttachmentIdMapping[url]);
|
||||
|
||||
if (imageNote) {
|
||||
updatedContent = replaceUrl(updatedContent, url, imageNote);
|
||||
@@ -614,19 +610,19 @@ function downloadImages(noteId: string, content: string) {
|
||||
}
|
||||
|
||||
function saveAttachments(note: BNote, content: string) {
|
||||
const inlineAttachmentRe = /<a[^>]*?\shref=['"]data:([^;'">]+);base64,([^'">]+)['"][^>]*>(.*?)<\/a>/igm;
|
||||
const inlineAttachmentRe = /<a[^>]*?\shref=['"]data:([^;'">]+);base64,([^'">]+)['"][^>]*>(.*?)<\/a>/gim;
|
||||
let attachmentMatch;
|
||||
|
||||
while (attachmentMatch = inlineAttachmentRe.exec(content)) {
|
||||
while ((attachmentMatch = inlineAttachmentRe.exec(content))) {
|
||||
const mime = attachmentMatch[1].toLowerCase();
|
||||
|
||||
const base64data = attachmentMatch[2];
|
||||
const buffer = Buffer.from(base64data, 'base64');
|
||||
const buffer = Buffer.from(base64data, "base64");
|
||||
|
||||
const title = html2plaintext(attachmentMatch[3]);
|
||||
|
||||
const attachment = note.saveAttachment({
|
||||
role: 'file',
|
||||
role: "file",
|
||||
mime: mime,
|
||||
title: title,
|
||||
content: buffer
|
||||
@@ -643,8 +639,7 @@ function saveAttachments(note: BNote, content: string) {
|
||||
}
|
||||
|
||||
function saveLinks(note: BNote, content: string | Buffer) {
|
||||
if ((note.type !== 'text' && note.type !== 'relationMap')
|
||||
|| (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
|
||||
if ((note.type !== "text" && note.type !== "relationMap") || (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
|
||||
return {
|
||||
forceFrontendReload: false,
|
||||
content
|
||||
@@ -654,7 +649,7 @@ function saveLinks(note: BNote, content: string | Buffer) {
|
||||
const foundLinks: FoundLink[] = [];
|
||||
let forceFrontendReload = false;
|
||||
|
||||
if (note.type === 'text' && typeof content === "string") {
|
||||
if (note.type === "text" && typeof content === "string") {
|
||||
content = downloadImages(note.noteId, content);
|
||||
content = saveAttachments(note, content);
|
||||
|
||||
@@ -662,17 +657,14 @@ function saveLinks(note: BNote, content: string | Buffer) {
|
||||
content = findInternalLinks(content, foundLinks);
|
||||
content = findIncludeNoteLinks(content, foundLinks);
|
||||
|
||||
({forceFrontendReload, content} = checkImageAttachments(note, content));
|
||||
}
|
||||
else if (note.type === 'relationMap' && typeof content === "string") {
|
||||
({ forceFrontendReload, content } = checkImageAttachments(note, content));
|
||||
} else if (note.type === "relationMap" && typeof content === "string") {
|
||||
findRelationMapLinks(content, foundLinks);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(`Unrecognized type '${note.type}'`);
|
||||
}
|
||||
|
||||
const existingLinks = note.getRelations().filter(rel =>
|
||||
['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(rel.name));
|
||||
const existingLinks = note.getRelations().filter((rel) => ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(rel.name));
|
||||
|
||||
for (const foundLink of foundLinks) {
|
||||
const targetNote = becca.notes[foundLink.value];
|
||||
@@ -680,16 +672,14 @@ function saveLinks(note: BNote, content: string | Buffer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingLink = existingLinks.find(existingLink =>
|
||||
existingLink.value === foundLink.value
|
||||
&& existingLink.name === foundLink.name);
|
||||
const existingLink = existingLinks.find((existingLink) => existingLink.value === foundLink.value && existingLink.name === foundLink.name);
|
||||
|
||||
if (!existingLink) {
|
||||
const newLink = new BAttribute({
|
||||
noteId: note.noteId,
|
||||
type: 'relation',
|
||||
type: "relation",
|
||||
name: foundLink.name,
|
||||
value: foundLink.value,
|
||||
value: foundLink.value
|
||||
}).save();
|
||||
|
||||
existingLinks.push(newLink);
|
||||
@@ -698,9 +688,7 @@ function saveLinks(note: BNote, content: string | Buffer) {
|
||||
}
|
||||
|
||||
// marking links as deleted if they are not present on the page anymore
|
||||
const unusedLinks = existingLinks.filter(existingLink => !foundLinks.some(foundLink =>
|
||||
existingLink.value === foundLink.value
|
||||
&& existingLink.name === foundLink.name));
|
||||
const unusedLinks = existingLinks.filter((existingLink) => !foundLinks.some((foundLink) => existingLink.value === foundLink.value && existingLink.name === foundLink.name));
|
||||
|
||||
for (const unusedLink of unusedLinks) {
|
||||
unusedLink.markAsDeleted();
|
||||
@@ -711,17 +699,16 @@ function saveLinks(note: BNote, content: string | Buffer) {
|
||||
|
||||
function saveRevisionIfNeeded(note: BNote) {
|
||||
// files and images are versioned separately
|
||||
if (note.type === 'file' || note.type === 'image' || note.isLabelTruthy('disableVersioning')) {
|
||||
if (note.type === "file" || note.type === "image" || note.isLabelTruthy("disableVersioning")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const revisionSnapshotTimeInterval = parseInt(optionService.getOption('revisionSnapshotTimeInterval'));
|
||||
const revisionSnapshotTimeInterval = parseInt(optionService.getOption("revisionSnapshotTimeInterval"));
|
||||
|
||||
const revisionCutoff = dateUtils.utcDateTimeStr(new Date(now.getTime() - revisionSnapshotTimeInterval * 1000));
|
||||
|
||||
const existingRevisionId = sql.getValue(
|
||||
"SELECT revisionId FROM revisions WHERE noteId = ? AND utcDateCreated >= ?", [note.noteId, revisionCutoff]);
|
||||
const existingRevisionId = sql.getValue("SELECT revisionId FROM revisions WHERE noteId = ? AND utcDateCreated >= ?", [note.noteId, revisionCutoff]);
|
||||
|
||||
const msSinceDateCreated = now.getTime() - dateUtils.parseDateTime(note.utcDateCreated).getTime();
|
||||
|
||||
@@ -744,18 +731,18 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
|
||||
note.setContent(newContent, { forceFrontendReload });
|
||||
|
||||
if (attachments?.length > 0) {
|
||||
const existingAttachmentsByTitle = toMap(note.getAttachments({includeContentLength: false}), 'title');
|
||||
const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
|
||||
|
||||
for (const {attachmentId, role, mime, title, position, content} of attachments) {
|
||||
for (const { attachmentId, role, mime, title, position, content } of attachments) {
|
||||
if (attachmentId || !(title in existingAttachmentsByTitle)) {
|
||||
note.saveAttachment({attachmentId, role, mime, title, content, position});
|
||||
note.saveAttachment({ attachmentId, role, mime, title, content, position });
|
||||
} else {
|
||||
const existingAttachment = existingAttachmentsByTitle[title];
|
||||
existingAttachment.role = role;
|
||||
existingAttachment.mime = mime;
|
||||
existingAttachment.position = position;
|
||||
if (content) {
|
||||
existingAttachment.setContent(content, {forceSave: true});
|
||||
existingAttachment.setContent(content, { forceSave: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -783,7 +770,7 @@ function undeleteNote(noteId: string, taskContext: TaskContext) {
|
||||
}
|
||||
|
||||
function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskContext) {
|
||||
const branchRow = sql.getRow<BranchRow>("SELECT * FROM branches WHERE branchId = ?", [branchId])
|
||||
const branchRow = sql.getRow<BranchRow>("SELECT * FROM branches WHERE branchId = ?", [branchId]);
|
||||
|
||||
if (!branchRow.isDeleted) {
|
||||
return;
|
||||
@@ -809,34 +796,43 @@ function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskCon
|
||||
noteEntity.updateFromRow(noteRow);
|
||||
noteEntity.save();
|
||||
|
||||
const attributeRows = sql.getRows<AttributeRow>(`
|
||||
const attributeRows = sql.getRows<AttributeRow>(
|
||||
`
|
||||
SELECT * FROM attributes
|
||||
WHERE isDeleted = 1
|
||||
AND deleteId = ?
|
||||
AND (noteId = ?
|
||||
OR (type = 'relation' AND value = ?))`, [deleteId, noteRow.noteId, noteRow.noteId]);
|
||||
OR (type = 'relation' AND value = ?))`,
|
||||
[deleteId, noteRow.noteId, noteRow.noteId]
|
||||
);
|
||||
|
||||
for (const attributeRow of attributeRows) {
|
||||
// relation might point to a note which hasn't been undeleted yet and would thus throw up
|
||||
new BAttribute(attributeRow).save({skipValidation: true});
|
||||
new BAttribute(attributeRow).save({ skipValidation: true });
|
||||
}
|
||||
|
||||
const attachmentRows = sql.getRows<AttachmentRow>(`
|
||||
const attachmentRows = sql.getRows<AttachmentRow>(
|
||||
`
|
||||
SELECT * FROM attachments
|
||||
WHERE isDeleted = 1
|
||||
AND deleteId = ?
|
||||
AND ownerId = ?`, [deleteId, noteRow.noteId]);
|
||||
AND ownerId = ?`,
|
||||
[deleteId, noteRow.noteId]
|
||||
);
|
||||
|
||||
for (const attachmentRow of attachmentRows) {
|
||||
new BAttachment(attachmentRow).save();
|
||||
}
|
||||
|
||||
const childBranchIds = sql.getColumn<string>(`
|
||||
const childBranchIds = sql.getColumn<string>(
|
||||
`
|
||||
SELECT branches.branchId
|
||||
FROM branches
|
||||
WHERE branches.isDeleted = 1
|
||||
AND branches.deleteId = ?
|
||||
AND branches.parentNoteId = ?`, [deleteId, noteRow.noteId]);
|
||||
AND branches.parentNoteId = ?`,
|
||||
[deleteId, noteRow.noteId]
|
||||
);
|
||||
|
||||
for (const childBranchId of childBranchIds) {
|
||||
undeleteBranch(childBranchId, deleteId, taskContext);
|
||||
@@ -848,18 +844,21 @@ function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskCon
|
||||
* @returns return deleted branchIds of an undeleted parent note
|
||||
*/
|
||||
function getUndeletedParentBranchIds(noteId: string, deleteId: string) {
|
||||
return sql.getColumn<string>(`
|
||||
return sql.getColumn<string>(
|
||||
`
|
||||
SELECT branches.branchId
|
||||
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`, [noteId, deleteId]);
|
||||
AND parentNote.isDeleted = 0`,
|
||||
[noteId, deleteId]
|
||||
);
|
||||
}
|
||||
|
||||
function scanForLinks(note: BNote, content: string | Buffer) {
|
||||
if (!note || !['text', 'relationMap'].includes(note.type)) {
|
||||
if (!note || !["text", "relationMap"].includes(note.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -871,8 +870,7 @@ function scanForLinks(note: BNote, content: string | Buffer) {
|
||||
note.setContent(newContent, { forceFrontendReload });
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
log.error(`Could not scan for links note '${note.noteId}': ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
@@ -899,21 +897,21 @@ function replaceByMap(str: string, mapObj: Record<string, string>) {
|
||||
return str;
|
||||
}
|
||||
|
||||
const re = new RegExp(Object.keys(mapObj).join("|"),"g");
|
||||
const re = new RegExp(Object.keys(mapObj).join("|"), "g");
|
||||
|
||||
return str.replace(re, matched => mapObj[matched]);
|
||||
return str.replace(re, (matched) => mapObj[matched]);
|
||||
}
|
||||
|
||||
function duplicateSubtree(origNoteId: string, newParentNoteId: string) {
|
||||
if (origNoteId === 'root') {
|
||||
throw new Error('Duplicating root is not possible');
|
||||
if (origNoteId === "root") {
|
||||
throw new Error("Duplicating root is not possible");
|
||||
}
|
||||
|
||||
log.info(`Duplicating '${origNoteId}' subtree into '${newParentNoteId}'`);
|
||||
|
||||
const origNote = becca.notes[origNoteId];
|
||||
// might be null if orig note is not in the target newParentNoteId
|
||||
const origBranch = origNote.getParentBranches().find(branch => branch.parentNoteId === newParentNoteId);
|
||||
const origBranch = origNote.getParentBranches().find((branch) => branch.parentNoteId === newParentNoteId);
|
||||
|
||||
const noteIdMapping = getNoteIdMapping(origNote);
|
||||
|
||||
@@ -935,8 +933,8 @@ function duplicateSubtree(origNoteId: string, newParentNoteId: string) {
|
||||
}
|
||||
|
||||
function duplicateSubtreeWithoutRoot(origNoteId: string, newNoteId: string) {
|
||||
if (origNoteId === 'root') {
|
||||
throw new Error('Duplicating root is not possible');
|
||||
if (origNoteId === "root") {
|
||||
throw new Error("Duplicating root is not possible");
|
||||
}
|
||||
|
||||
const origNote = becca.getNote(origNoteId);
|
||||
@@ -978,7 +976,7 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch, newParentNo
|
||||
|
||||
let content = origNote.getContent();
|
||||
|
||||
if (typeof content === "string" && ['text', 'relationMap', 'search'].includes(origNote.type)) {
|
||||
if (typeof content === "string" && ["text", "relationMap", "search"].includes(origNote.type)) {
|
||||
// fix links in the content
|
||||
content = replaceByMap(content, noteIdMapping);
|
||||
}
|
||||
@@ -994,12 +992,12 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch, newParentNo
|
||||
|
||||
// if relation points to within the duplicated tree then replace the target to the duplicated note
|
||||
// if it points outside of duplicated tree then keep the original target
|
||||
if (attr.type === 'relation' && attr.value in noteIdMapping) {
|
||||
if (attr.type === "relation" && attr.value in noteIdMapping) {
|
||||
attr.value = noteIdMapping[attr.value];
|
||||
}
|
||||
|
||||
// the relation targets may not be created yet, the mapping is pre-generated
|
||||
attr.save({skipValidation: true});
|
||||
attr.save({ skipValidation: true });
|
||||
}
|
||||
|
||||
for (const childBranch of origNote.getChildBranches()) {
|
||||
@@ -1013,20 +1011,20 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch, newParentNo
|
||||
|
||||
const existingNote = becca.notes[newNoteId];
|
||||
|
||||
if (existingNote && existingNote.title !== undefined) { // checking that it's not just note's skeleton created because of Branch above
|
||||
if (existingNote && existingNote.title !== undefined) {
|
||||
// checking that it's not just note's skeleton created because of Branch above
|
||||
// note has multiple clones and was already created from another placement in the tree,
|
||||
// so a branch is all we need for this clone
|
||||
return {
|
||||
note: existingNote,
|
||||
branch: createDuplicatedBranch()
|
||||
}
|
||||
}
|
||||
else {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
// order here is important, note needs to be created first to not mess up the becca
|
||||
note: createDuplicatedNote(),
|
||||
branch: createDuplicatedBranch()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import becca from "../becca/becca.js";
|
||||
import BOption from "../becca/entities/boption.js";
|
||||
import { OptionRow } from '../becca/entities/rows.js';
|
||||
import { OptionRow } from "../becca/entities/rows.js";
|
||||
import { FilterOptionsByType, OptionDefinitions, OptionMap, OptionNames } from "./options_interface.js";
|
||||
import sql from "./sql.js";
|
||||
|
||||
@@ -60,11 +60,11 @@ function getOptionInt(name: FilterOptionsByType<number>, defaultValue?: number):
|
||||
function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
|
||||
const val = getOption(name);
|
||||
|
||||
if (typeof val !== "string" || !['true', 'false'].includes(val)) {
|
||||
if (typeof val !== "string" || !["true", "false"].includes(val)) {
|
||||
throw new Error(`Could not parse '${val}' into boolean for option '${name}'`);
|
||||
}
|
||||
|
||||
return val === 'true';
|
||||
return val === "true";
|
||||
}
|
||||
|
||||
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
|
||||
@@ -74,8 +74,7 @@ function setOption<T extends OptionNames>(name: T, value: string | OptionDefinit
|
||||
option.value = value as string;
|
||||
|
||||
option.save();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
createOption(name, value, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { randomSecureToken, isWindows } from "./utils.js";
|
||||
import log from "./log.js";
|
||||
import dateUtils from "./date_utils.js";
|
||||
import keyboardActions from "./keyboard_actions.js";
|
||||
import { KeyboardShortcutWithRequiredActionName } from './keyboard_actions_interface.js';
|
||||
import { KeyboardShortcutWithRequiredActionName } from "./keyboard_actions_interface.js";
|
||||
|
||||
function initDocumentOptions() {
|
||||
optionService.createOption('documentId', randomSecureToken(16), false);
|
||||
optionService.createOption('documentSecret', randomSecureToken(16), false);
|
||||
optionService.createOption("documentId", randomSecureToken(16), false);
|
||||
optionService.createOption("documentSecret", randomSecureToken(16), false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,10 +26,10 @@ interface NotSyncedOpts {
|
||||
interface DefaultOption {
|
||||
name: OptionNames;
|
||||
/**
|
||||
* The value to initialize the option with, if the option is not already present in the database.
|
||||
*
|
||||
* If a function is passed Gin instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized.
|
||||
*/
|
||||
* The value to initialize the option with, if the option is not already present in the database.
|
||||
*
|
||||
* If a function is passed Gin instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized.
|
||||
*/
|
||||
value: string | ((options: OptionMap) => string);
|
||||
isSynced: boolean;
|
||||
}
|
||||
@@ -41,95 +41,107 @@ interface DefaultOption {
|
||||
* @param opts additional options to be initialized, for example the sync configuration.
|
||||
*/
|
||||
async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = {}) {
|
||||
optionService.createOption('openNoteContexts', JSON.stringify([
|
||||
{
|
||||
notePath: 'root',
|
||||
active: true
|
||||
}
|
||||
]), false);
|
||||
optionService.createOption(
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: "root",
|
||||
active: true
|
||||
}
|
||||
]),
|
||||
false
|
||||
);
|
||||
|
||||
optionService.createOption('lastDailyBackupDate', dateUtils.utcNowDateTime(), false);
|
||||
optionService.createOption('lastWeeklyBackupDate', dateUtils.utcNowDateTime(), false);
|
||||
optionService.createOption('lastMonthlyBackupDate', dateUtils.utcNowDateTime(), false);
|
||||
optionService.createOption('dbVersion', appInfo.dbVersion.toString(), false);
|
||||
optionService.createOption("lastDailyBackupDate", dateUtils.utcNowDateTime(), false);
|
||||
optionService.createOption("lastWeeklyBackupDate", dateUtils.utcNowDateTime(), false);
|
||||
optionService.createOption("lastMonthlyBackupDate", dateUtils.utcNowDateTime(), false);
|
||||
optionService.createOption("dbVersion", appInfo.dbVersion.toString(), false);
|
||||
|
||||
optionService.createOption('initialized', initialized ? 'true' : 'false', false);
|
||||
optionService.createOption("initialized", initialized ? "true" : "false", false);
|
||||
|
||||
optionService.createOption('lastSyncedPull', '0', false);
|
||||
optionService.createOption('lastSyncedPush', '0', false);
|
||||
optionService.createOption("lastSyncedPull", "0", false);
|
||||
optionService.createOption("lastSyncedPush", "0", false);
|
||||
|
||||
optionService.createOption('theme', 'next', false);
|
||||
optionService.createOption("theme", "next", false);
|
||||
|
||||
optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
|
||||
optionService.createOption('syncServerTimeout', '120000', false);
|
||||
optionService.createOption('syncProxy', opts.syncProxy || '', false);
|
||||
optionService.createOption("syncServerHost", opts.syncServerHost || "", false);
|
||||
optionService.createOption("syncServerTimeout", "120000", false);
|
||||
optionService.createOption("syncProxy", opts.syncProxy || "", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains all the default options that must be initialized on new and existing databases (at startup). The value can also be determined based on other options, provided they have already been initialized.
|
||||
*/
|
||||
const defaultOptions: DefaultOption[] = [
|
||||
{ name: 'revisionSnapshotTimeInterval', value: '600', isSynced: true },
|
||||
{ name: 'revisionSnapshotNumberLimit', value: '-1', isSynced: true },
|
||||
{ name: 'protectedSessionTimeout', value: '600', isSynced: true },
|
||||
{ name: 'zoomFactor', value: isWindows() ? '0.9' : '1.0', isSynced: false },
|
||||
{ name: 'overrideThemeFonts', value: 'false', isSynced: false },
|
||||
{ name: 'mainFontFamily', value: 'theme', isSynced: false },
|
||||
{ name: 'mainFontSize', value: '100', isSynced: false },
|
||||
{ name: 'treeFontFamily', value: 'theme', isSynced: false },
|
||||
{ name: 'treeFontSize', value: '100', isSynced: false },
|
||||
{ name: 'detailFontFamily', value: 'theme', isSynced: false },
|
||||
{ name: 'detailFontSize', value: '110', isSynced: false },
|
||||
{ name: 'monospaceFontFamily', value: 'theme', isSynced: false },
|
||||
{ name: 'monospaceFontSize', value: '110', isSynced: false },
|
||||
{ name: 'spellCheckEnabled', value: 'true', isSynced: false },
|
||||
{ name: 'spellCheckLanguageCode', value: 'en-US', isSynced: false },
|
||||
{ name: 'imageMaxWidthHeight', value: '2000', isSynced: true },
|
||||
{ name: 'imageJpegQuality', value: '75', isSynced: true },
|
||||
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
|
||||
{ name: 'vimKeymapEnabled', value: 'false', isSynced: false },
|
||||
{ name: 'codeLineWrapEnabled', value: 'true', isSynced: false },
|
||||
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml","text/x-sh"]', isSynced: true },
|
||||
{ name: 'leftPaneWidth', value: '25', isSynced: false },
|
||||
{ name: 'leftPaneVisible', value: 'true', isSynced: false },
|
||||
{ name: 'rightPaneWidth', value: '25', isSynced: false },
|
||||
{ name: 'rightPaneVisible', value: 'true', isSynced: false },
|
||||
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false },
|
||||
{ name: 'eraseEntitiesAfterTimeInSeconds', value: '604800', isSynced: true }, // default is 7 days
|
||||
{ name: 'hideArchivedNotes_main', value: 'false', isSynced: false },
|
||||
{ name: 'debugModeEnabled', value: 'false', isSynced: false },
|
||||
{ name: 'headingStyle', value: 'underline', isSynced: true },
|
||||
{ name: 'autoCollapseNoteTree', value: 'true', isSynced: true },
|
||||
{ name: 'autoReadonlySizeText', value: '10000', isSynced: false },
|
||||
{ name: 'autoReadonlySizeCode', value: '30000', isSynced: false },
|
||||
{ name: 'dailyBackupEnabled', value: 'true', isSynced: false },
|
||||
{ name: 'weeklyBackupEnabled', value: 'true', isSynced: false },
|
||||
{ name: 'monthlyBackupEnabled', value: 'true', isSynced: false },
|
||||
{ name: 'maxContentWidth', value: '1200', isSynced: false },
|
||||
{ name: 'compressImages', value: 'true', isSynced: true },
|
||||
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
|
||||
{ name: 'minTocHeadings', value: '5', isSynced: true },
|
||||
{ name: 'highlightsList', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
|
||||
{ name: 'checkForUpdates', value: 'true', isSynced: true },
|
||||
{ name: 'disableTray', value: 'false', isSynced: false },
|
||||
{ name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true },
|
||||
{ name: 'customSearchEngineName', value: 'DuckDuckGo', isSynced: true },
|
||||
{ name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true },
|
||||
{ name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true },
|
||||
{ name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true },
|
||||
{ name: "revisionSnapshotTimeInterval", value: "600", isSynced: true },
|
||||
{ name: "revisionSnapshotNumberLimit", value: "-1", isSynced: true },
|
||||
{ name: "protectedSessionTimeout", value: "600", isSynced: true },
|
||||
{ name: "zoomFactor", value: isWindows() ? "0.9" : "1.0", isSynced: false },
|
||||
{ name: "overrideThemeFonts", value: "false", isSynced: false },
|
||||
{ name: "mainFontFamily", value: "theme", isSynced: false },
|
||||
{ name: "mainFontSize", value: "100", isSynced: false },
|
||||
{ name: "treeFontFamily", value: "theme", isSynced: false },
|
||||
{ name: "treeFontSize", value: "100", isSynced: false },
|
||||
{ name: "detailFontFamily", value: "theme", isSynced: false },
|
||||
{ name: "detailFontSize", value: "110", isSynced: false },
|
||||
{ name: "monospaceFontFamily", value: "theme", isSynced: false },
|
||||
{ name: "monospaceFontSize", value: "110", isSynced: false },
|
||||
{ name: "spellCheckEnabled", value: "true", isSynced: false },
|
||||
{ name: "spellCheckLanguageCode", value: "en-US", isSynced: false },
|
||||
{ name: "imageMaxWidthHeight", value: "2000", isSynced: true },
|
||||
{ name: "imageJpegQuality", value: "75", isSynced: true },
|
||||
{ name: "autoFixConsistencyIssues", value: "true", isSynced: false },
|
||||
{ name: "vimKeymapEnabled", value: "false", isSynced: false },
|
||||
{ name: "codeLineWrapEnabled", value: "true", isSynced: false },
|
||||
{
|
||||
name: "codeNotesMimeTypes",
|
||||
value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml","text/x-sh"]',
|
||||
isSynced: true
|
||||
},
|
||||
{ name: "leftPaneWidth", value: "25", isSynced: false },
|
||||
{ name: "leftPaneVisible", value: "true", isSynced: false },
|
||||
{ name: "rightPaneWidth", value: "25", isSynced: false },
|
||||
{ name: "rightPaneVisible", value: "true", isSynced: false },
|
||||
{ name: "nativeTitleBarVisible", value: "false", isSynced: false },
|
||||
{ name: "eraseEntitiesAfterTimeInSeconds", value: "604800", isSynced: true }, // default is 7 days
|
||||
{ name: "hideArchivedNotes_main", value: "false", isSynced: false },
|
||||
{ name: "debugModeEnabled", value: "false", isSynced: false },
|
||||
{ name: "headingStyle", value: "underline", isSynced: true },
|
||||
{ name: "autoCollapseNoteTree", value: "true", isSynced: true },
|
||||
{ name: "autoReadonlySizeText", value: "10000", isSynced: false },
|
||||
{ name: "autoReadonlySizeCode", value: "30000", isSynced: false },
|
||||
{ name: "dailyBackupEnabled", value: "true", isSynced: false },
|
||||
{ name: "weeklyBackupEnabled", value: "true", isSynced: false },
|
||||
{ name: "monthlyBackupEnabled", value: "true", isSynced: false },
|
||||
{ name: "maxContentWidth", value: "1200", isSynced: false },
|
||||
{ name: "compressImages", value: "true", isSynced: true },
|
||||
{ name: "downloadImagesAutomatically", value: "true", isSynced: true },
|
||||
{ name: "minTocHeadings", value: "5", isSynced: true },
|
||||
{ name: "highlightsList", value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
|
||||
{ name: "checkForUpdates", value: "true", isSynced: true },
|
||||
{ name: "disableTray", value: "false", isSynced: false },
|
||||
{ name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true },
|
||||
{ name: "customSearchEngineName", value: "DuckDuckGo", isSynced: true },
|
||||
{ name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true },
|
||||
{ name: "promotedAttributesOpenInRibbon", value: "true", isSynced: true },
|
||||
{ name: "editedNotesOpenInRibbon", value: "true", isSynced: true },
|
||||
|
||||
// Internationalization
|
||||
{ name: 'locale', value: 'en', isSynced: true },
|
||||
{ name: 'firstDayOfWeek', value: '1', isSynced: true },
|
||||
{ name: "locale", value: "en", isSynced: true },
|
||||
{ name: "firstDayOfWeek", value: "1", isSynced: true },
|
||||
|
||||
// Code block configuration
|
||||
{ name: "codeBlockTheme", value: (optionsMap) => {
|
||||
if (optionsMap.theme === "light") {
|
||||
return "default:stackoverflow-light";
|
||||
} else {
|
||||
return "default:stackoverflow-dark";
|
||||
}
|
||||
}, isSynced: false },
|
||||
{
|
||||
name: "codeBlockTheme",
|
||||
value: (optionsMap) => {
|
||||
if (optionsMap.theme === "light") {
|
||||
return "default:stackoverflow-light";
|
||||
} else {
|
||||
return "default:stackoverflow-dark";
|
||||
}
|
||||
},
|
||||
isSynced: false
|
||||
},
|
||||
{ name: "codeBlockWordWrap", value: "false", isSynced: true },
|
||||
|
||||
// Text note configuration
|
||||
@@ -139,18 +151,106 @@ const defaultOptions: DefaultOption[] = [
|
||||
// HTML import configuration
|
||||
{ name: "layoutOrientation", value: "vertical", isSynced: false },
|
||||
{ name: "backgroundEffects", value: "false", isSynced: false },
|
||||
{ name: "allowedHtmlTags", value: JSON.stringify([
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
|
||||
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
|
||||
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
|
||||
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
|
||||
'en-media',
|
||||
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
|
||||
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
|
||||
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
|
||||
]), isSynced: true },
|
||||
{
|
||||
name: "allowedHtmlTags",
|
||||
value: JSON.stringify([
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"blockquote",
|
||||
"p",
|
||||
"a",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"b",
|
||||
"i",
|
||||
"strong",
|
||||
"em",
|
||||
"strike",
|
||||
"s",
|
||||
"del",
|
||||
"abbr",
|
||||
"code",
|
||||
"hr",
|
||||
"br",
|
||||
"div",
|
||||
"table",
|
||||
"thead",
|
||||
"caption",
|
||||
"tbody",
|
||||
"tfoot",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
"pre",
|
||||
"section",
|
||||
"img",
|
||||
"figure",
|
||||
"figcaption",
|
||||
"span",
|
||||
"label",
|
||||
"input",
|
||||
"details",
|
||||
"summary",
|
||||
"address",
|
||||
"aside",
|
||||
"footer",
|
||||
"header",
|
||||
"hgroup",
|
||||
"main",
|
||||
"nav",
|
||||
"dl",
|
||||
"dt",
|
||||
"menu",
|
||||
"bdi",
|
||||
"bdo",
|
||||
"dfn",
|
||||
"kbd",
|
||||
"mark",
|
||||
"q",
|
||||
"time",
|
||||
"var",
|
||||
"wbr",
|
||||
"area",
|
||||
"map",
|
||||
"track",
|
||||
"video",
|
||||
"audio",
|
||||
"picture",
|
||||
"del",
|
||||
"ins",
|
||||
"en-media",
|
||||
"acronym",
|
||||
"article",
|
||||
"big",
|
||||
"button",
|
||||
"cite",
|
||||
"col",
|
||||
"colgroup",
|
||||
"data",
|
||||
"dd",
|
||||
"fieldset",
|
||||
"form",
|
||||
"legend",
|
||||
"meter",
|
||||
"noscript",
|
||||
"option",
|
||||
"progress",
|
||||
"rp",
|
||||
"samp",
|
||||
"small",
|
||||
"sub",
|
||||
"sup",
|
||||
"template",
|
||||
"textarea",
|
||||
"tt"
|
||||
]),
|
||||
isSynced: true
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -163,7 +263,7 @@ function initStartupOptions() {
|
||||
|
||||
const allDefaultOptions = defaultOptions.concat(getKeyboardDefaultOptions());
|
||||
|
||||
for (const {name, value, isSynced} of allDefaultOptions) {
|
||||
for (const { name, value, isSynced } of allDefaultOptions) {
|
||||
if (!(name in optionsMap)) {
|
||||
let resolvedValue;
|
||||
if (typeof value === "function") {
|
||||
@@ -178,23 +278,24 @@ function initStartupOptions() {
|
||||
}
|
||||
|
||||
if (process.env.TRILIUM_START_NOTE_ID || process.env.TRILIUM_SAFE_MODE) {
|
||||
optionService.setOption('openNoteContexts', JSON.stringify([
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || 'root',
|
||||
active: true
|
||||
}
|
||||
]));
|
||||
optionService.setOption(
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getKeyboardDefaultOptions() {
|
||||
return (keyboardActions.getDefaultKeyboardActions()
|
||||
.filter(ka => !!ka.actionName) as KeyboardShortcutWithRequiredActionName[])
|
||||
.map(ka => ({
|
||||
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
|
||||
value: JSON.stringify(ka.defaultShortcuts),
|
||||
isSynced: false
|
||||
})) as DefaultOption[];
|
||||
return (keyboardActions.getDefaultKeyboardActions().filter((ka) => !!ka.actionName) as KeyboardShortcutWithRequiredActionName[]).map((ka) => ({
|
||||
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
|
||||
value: JSON.stringify(ka.defaultShortcuts),
|
||||
isSynced: false
|
||||
})) as DefaultOption[];
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -9,90 +9,90 @@ export type OptionMap = Record<OptionNames, string>;
|
||||
* For each keyboard action, there is a corresponding option which identifies the key combination defined by the user.
|
||||
*/
|
||||
type KeyboardShortcutsOptions<T extends KeyboardActionNames> = {
|
||||
[key in T as `keyboardShortcuts${Capitalize<key>}`]: string
|
||||
[key in T as `keyboardShortcuts${Capitalize<key>}`]: string;
|
||||
};
|
||||
|
||||
export type FontFamily = "theme" | "serif" | "sans-serif" | "monospace" | string;
|
||||
|
||||
export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActionNames> {
|
||||
"openNoteContexts": string;
|
||||
"lastDailyBackupDate": string;
|
||||
"lastWeeklyBackupDate": string;
|
||||
"lastMonthlyBackupDate": string;
|
||||
"dbVersion": string;
|
||||
"theme": string;
|
||||
"syncServerHost": string;
|
||||
"syncServerTimeout": string;
|
||||
"syncProxy": string;
|
||||
"mainFontFamily": FontFamily;
|
||||
"treeFontFamily": FontFamily;
|
||||
"detailFontFamily": FontFamily;
|
||||
"monospaceFontFamily": FontFamily;
|
||||
"spellCheckLanguageCode": string;
|
||||
"codeNotesMimeTypes": string;
|
||||
"headingStyle": string;
|
||||
"highlightsList": string;
|
||||
"customSearchEngineName": string;
|
||||
"customSearchEngineUrl": string;
|
||||
"locale": string;
|
||||
"codeBlockTheme": string;
|
||||
"textNoteEditorType": string;
|
||||
"layoutOrientation": string;
|
||||
"allowedHtmlTags": string;
|
||||
"documentId": string;
|
||||
"documentSecret": string;
|
||||
"passwordVerificationHash": string;
|
||||
"passwordVerificationSalt": string;
|
||||
"passwordDerivedKeySalt": string;
|
||||
"encryptedDataKey": string;
|
||||
openNoteContexts: string;
|
||||
lastDailyBackupDate: string;
|
||||
lastWeeklyBackupDate: string;
|
||||
lastMonthlyBackupDate: string;
|
||||
dbVersion: string;
|
||||
theme: string;
|
||||
syncServerHost: string;
|
||||
syncServerTimeout: string;
|
||||
syncProxy: string;
|
||||
mainFontFamily: FontFamily;
|
||||
treeFontFamily: FontFamily;
|
||||
detailFontFamily: FontFamily;
|
||||
monospaceFontFamily: FontFamily;
|
||||
spellCheckLanguageCode: string;
|
||||
codeNotesMimeTypes: string;
|
||||
headingStyle: string;
|
||||
highlightsList: string;
|
||||
customSearchEngineName: string;
|
||||
customSearchEngineUrl: string;
|
||||
locale: string;
|
||||
codeBlockTheme: string;
|
||||
textNoteEditorType: string;
|
||||
layoutOrientation: string;
|
||||
allowedHtmlTags: string;
|
||||
documentId: string;
|
||||
documentSecret: string;
|
||||
passwordVerificationHash: string;
|
||||
passwordVerificationSalt: string;
|
||||
passwordDerivedKeySalt: string;
|
||||
encryptedDataKey: string;
|
||||
|
||||
"lastSyncedPull": number;
|
||||
"lastSyncedPush": number;
|
||||
"revisionSnapshotTimeInterval": number;
|
||||
"revisionSnapshotNumberLimit": number;
|
||||
"protectedSessionTimeout": number;
|
||||
"zoomFactor": number;
|
||||
"mainFontSize": number;
|
||||
"treeFontSize": number;
|
||||
"detailFontSize": number;
|
||||
"monospaceFontSize": number;
|
||||
"imageMaxWidthHeight": number;
|
||||
"imageJpegQuality": number;
|
||||
"leftPaneWidth": number;
|
||||
"rightPaneWidth": number;
|
||||
"eraseEntitiesAfterTimeInSeconds": number;
|
||||
"autoReadonlySizeText": number;
|
||||
"autoReadonlySizeCode": number;
|
||||
"maxContentWidth": number;
|
||||
"minTocHeadings": number;
|
||||
"eraseUnusedAttachmentsAfterSeconds": number;
|
||||
"firstDayOfWeek": number;
|
||||
lastSyncedPull: number;
|
||||
lastSyncedPush: number;
|
||||
revisionSnapshotTimeInterval: number;
|
||||
revisionSnapshotNumberLimit: number;
|
||||
protectedSessionTimeout: number;
|
||||
zoomFactor: number;
|
||||
mainFontSize: number;
|
||||
treeFontSize: number;
|
||||
detailFontSize: number;
|
||||
monospaceFontSize: number;
|
||||
imageMaxWidthHeight: number;
|
||||
imageJpegQuality: number;
|
||||
leftPaneWidth: number;
|
||||
rightPaneWidth: number;
|
||||
eraseEntitiesAfterTimeInSeconds: number;
|
||||
autoReadonlySizeText: number;
|
||||
autoReadonlySizeCode: number;
|
||||
maxContentWidth: number;
|
||||
minTocHeadings: number;
|
||||
eraseUnusedAttachmentsAfterSeconds: number;
|
||||
firstDayOfWeek: number;
|
||||
|
||||
"initialized": boolean;
|
||||
"overrideThemeFonts": boolean;
|
||||
"spellCheckEnabled": boolean;
|
||||
"autoFixConsistencyIssues": boolean;
|
||||
"vimKeymapEnabled": boolean;
|
||||
"codeLineWrapEnabled": boolean;
|
||||
"leftPaneVisible": boolean;
|
||||
"rightPaneVisible": boolean;
|
||||
"nativeTitleBarVisible": boolean;
|
||||
"hideArchivedNotes_main": boolean;
|
||||
"debugModeEnabled": boolean;
|
||||
"autoCollapseNoteTree": boolean;
|
||||
"dailyBackupEnabled": boolean;
|
||||
"weeklyBackupEnabled": boolean;
|
||||
"monthlyBackupEnabled": boolean;
|
||||
"compressImages": boolean;
|
||||
"downloadImagesAutomatically": boolean;
|
||||
"checkForUpdates": boolean;
|
||||
"disableTray": boolean;
|
||||
"promotedAttributesOpenInRibbon": boolean;
|
||||
"editedNotesOpenInRibbon": boolean;
|
||||
"codeBlockWordWrap": boolean;
|
||||
"textNoteEditorMultilineToolbar": boolean;
|
||||
"backgroundEffects": boolean;
|
||||
};
|
||||
initialized: boolean;
|
||||
overrideThemeFonts: boolean;
|
||||
spellCheckEnabled: boolean;
|
||||
autoFixConsistencyIssues: boolean;
|
||||
vimKeymapEnabled: boolean;
|
||||
codeLineWrapEnabled: boolean;
|
||||
leftPaneVisible: boolean;
|
||||
rightPaneVisible: boolean;
|
||||
nativeTitleBarVisible: boolean;
|
||||
hideArchivedNotes_main: boolean;
|
||||
debugModeEnabled: boolean;
|
||||
autoCollapseNoteTree: boolean;
|
||||
dailyBackupEnabled: boolean;
|
||||
weeklyBackupEnabled: boolean;
|
||||
monthlyBackupEnabled: boolean;
|
||||
compressImages: boolean;
|
||||
downloadImagesAutomatically: boolean;
|
||||
checkForUpdates: boolean;
|
||||
disableTray: boolean;
|
||||
promotedAttributesOpenInRibbon: boolean;
|
||||
editedNotesOpenInRibbon: boolean;
|
||||
codeBlockWordWrap: boolean;
|
||||
textNoteEditorMultilineToolbar: boolean;
|
||||
backgroundEffects: boolean;
|
||||
}
|
||||
|
||||
export type OptionNames = keyof OptionDefinitions;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ if (process.env.TRILIUM_PORT) {
|
||||
} else if (isElectron()) {
|
||||
port = env.isDev() ? 37740 : 37840;
|
||||
} else {
|
||||
port = parseAndValidate(config['Network']['port'] || '3000', `Network.port in ${dataDir.CONFIG_INI_PATH}`);
|
||||
port = parseAndValidate(config["Network"]["port"] || "3000", `Network.port in ${dataDir.CONFIG_INI_PATH}`);
|
||||
}
|
||||
|
||||
export default port;
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
import { DefinitionObject } from "./promoted_attribute_definition_interface.js";
|
||||
|
||||
function parse(value: string): DefinitionObject {
|
||||
const tokens = value.split(',').map(t => t.trim());
|
||||
const tokens = value.split(",").map((t) => t.trim());
|
||||
const defObj: DefinitionObject = {};
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token === 'promoted') {
|
||||
if (token === "promoted") {
|
||||
defObj.isPromoted = true;
|
||||
}
|
||||
else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
|
||||
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
|
||||
defObj.labelType = token;
|
||||
}
|
||||
else if (['single', 'multi'].includes(token)) {
|
||||
} else if (["single", "multi"].includes(token)) {
|
||||
defObj.multiplicity = token;
|
||||
}
|
||||
else if (token.startsWith('precision')) {
|
||||
const chunks = token.split('=');
|
||||
} else if (token.startsWith("precision")) {
|
||||
const chunks = token.split("=");
|
||||
|
||||
defObj.numberPrecision = parseInt(chunks[1]);
|
||||
}
|
||||
else if (token.startsWith('alias')) {
|
||||
const chunks = token.split('=');
|
||||
} else if (token.startsWith("alias")) {
|
||||
const chunks = token.split("=");
|
||||
|
||||
defObj.promotedAlias = chunks[1];
|
||||
}
|
||||
else if (token.startsWith('inverse')) {
|
||||
const chunks = token.split('=');
|
||||
} else if (token.startsWith("inverse")) {
|
||||
const chunks = token.split("=");
|
||||
|
||||
defObj.inverseRelation = chunks[1].replace(/[^\p{L}\p{N}_:]/ug, "")
|
||||
}
|
||||
else {
|
||||
defObj.inverseRelation = chunks[1].replace(/[^\p{L}\p{N}_:]/gu, "");
|
||||
} else {
|
||||
console.log("Unrecognized attribute definition token:", token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,8 @@ function touchProtectedSession() {
|
||||
}
|
||||
|
||||
function checkProtectedSessionExpiration() {
|
||||
const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout');
|
||||
if (isProtectedSessionAvailable()
|
||||
&& lastProtectedSessionOperationDate
|
||||
&& Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) {
|
||||
|
||||
const protectedSessionTimeout = options.getOptionInt("protectedSessionTimeout");
|
||||
if (isProtectedSessionAvailable() && lastProtectedSessionOperationDate && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) {
|
||||
resetDataKey();
|
||||
|
||||
log.info("Expiring protected session");
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isElectron } from "./utils.js";
|
||||
import log from "./log.js";
|
||||
import url from "url";
|
||||
import syncOptions from "./sync_options.js";
|
||||
import { ExecOpts } from './request_interface.js';
|
||||
import { ExecOpts } from "./request_interface.js";
|
||||
|
||||
// this service provides abstraction over node's HTTP/HTTPS and electron net.client APIs
|
||||
// this allows supporting system proxy
|
||||
@@ -22,7 +22,7 @@ interface ClientOpts {
|
||||
proxy?: string | null;
|
||||
}
|
||||
|
||||
type RequestEvent = ("error" | "response" | "abort");
|
||||
type RequestEvent = "error" | "response" | "abort";
|
||||
|
||||
interface Request {
|
||||
on(event: RequestEvent, cb: (e: any) => void): void;
|
||||
@@ -37,14 +37,14 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
|
||||
const client = getClient(opts);
|
||||
|
||||
// hack for cases where electron.net does not work, but we don't want to set proxy
|
||||
if (opts.proxy === 'noproxy') {
|
||||
if (opts.proxy === "noproxy") {
|
||||
opts.proxy = null;
|
||||
}
|
||||
|
||||
const paging = opts.paging || {
|
||||
pageCount: 1,
|
||||
pageIndex: 0,
|
||||
requestId: 'n/a'
|
||||
requestId: "n/a"
|
||||
};
|
||||
|
||||
const proxyAgent = await getProxyAgent(opts);
|
||||
@@ -54,14 +54,14 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
|
||||
try {
|
||||
const headers: Record<string, string | number> = {
|
||||
Cookie: (opts.cookieJar && opts.cookieJar.header) || "",
|
||||
'Content-Type': paging.pageCount === 1 ? 'application/json' : 'text/plain',
|
||||
"Content-Type": paging.pageCount === 1 ? "application/json" : "text/plain",
|
||||
pageCount: paging.pageCount,
|
||||
pageIndex: paging.pageIndex,
|
||||
requestId: paging.requestId
|
||||
};
|
||||
|
||||
if (opts.auth) {
|
||||
headers['trilium-cred'] = Buffer.from(`dummy:${opts.auth.password}`).toString('base64');
|
||||
headers["trilium-cred"] = Buffer.from(`dummy:${opts.auth.password}`).toString("base64");
|
||||
}
|
||||
|
||||
const request = (await client).request({
|
||||
@@ -78,22 +78,22 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
|
||||
agent: proxyAgent
|
||||
});
|
||||
|
||||
request.on('error', err => reject(generateError(opts, err)));
|
||||
request.on("error", (err) => reject(generateError(opts, err)));
|
||||
|
||||
request.on('response', response => {
|
||||
if (opts.cookieJar && response.headers['set-cookie']) {
|
||||
opts.cookieJar.header = response.headers['set-cookie'];
|
||||
request.on("response", (response) => {
|
||||
if (opts.cookieJar && response.headers["set-cookie"]) {
|
||||
opts.cookieJar.header = response.headers["set-cookie"];
|
||||
}
|
||||
|
||||
let responseStr = '';
|
||||
let responseStr = "";
|
||||
let chunks: Buffer[] = [];
|
||||
|
||||
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
response.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
|
||||
response.on('end', () => {
|
||||
response.on("end", () => {
|
||||
// use Buffer instead of string concatenation to avoid implicit decoding for each chunk
|
||||
// decode the entire data chunks explicitly as utf-8
|
||||
responseStr = Buffer.concat(chunks).toString('utf-8')
|
||||
responseStr = Buffer.concat(chunks).toString("utf-8");
|
||||
|
||||
if ([200, 201, 204].includes(response.statusCode)) {
|
||||
try {
|
||||
@@ -111,7 +111,7 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
|
||||
try {
|
||||
const jsonObj = JSON.parse(responseStr);
|
||||
|
||||
errorMessage = jsonObj?.message || '';
|
||||
errorMessage = jsonObj?.message || "";
|
||||
} catch (e: any) {
|
||||
errorMessage = responseStr.substr(0, Math.min(responseStr.length, 100));
|
||||
}
|
||||
@@ -124,14 +124,11 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
|
||||
let payload;
|
||||
|
||||
if (opts.body) {
|
||||
payload = typeof opts.body === 'object'
|
||||
? JSON.stringify(opts.body)
|
||||
: opts.body;
|
||||
payload = typeof opts.body === "object" ? JSON.stringify(opts.body) : opts.body;
|
||||
}
|
||||
|
||||
request.end(payload as string);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
reject(generateError(opts, e.message));
|
||||
}
|
||||
});
|
||||
@@ -140,7 +137,7 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
|
||||
async function getImage(imageUrl: string): Promise<Buffer> {
|
||||
const proxyConf = syncOptions.getSyncProxy();
|
||||
const opts: ClientOpts = {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
url: imageUrl,
|
||||
proxy: proxyConf !== "noproxy" ? proxyConf : null
|
||||
};
|
||||
@@ -165,19 +162,19 @@ async function getImage(imageUrl: string): Promise<Buffer> {
|
||||
agent: proxyAgent
|
||||
});
|
||||
|
||||
request.on('error', err => reject(generateError(opts, err)));
|
||||
request.on("error", (err) => reject(generateError(opts, err)));
|
||||
|
||||
request.on('abort', err => reject(generateError(opts, err)));
|
||||
request.on("abort", (err) => reject(generateError(opts, err)));
|
||||
|
||||
request.on('response', response => {
|
||||
request.on("response", (response) => {
|
||||
if (![200, 201, 204].includes(response.statusCode)) {
|
||||
reject(generateError(opts, `${response.statusCode} ${response.statusMessage}`));
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
response.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
response.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
response.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
|
||||
request.end(undefined);
|
||||
@@ -187,22 +184,21 @@ async function getImage(imageUrl: string): Promise<Buffer> {
|
||||
});
|
||||
}
|
||||
|
||||
const HTTP = 'http:', HTTPS = 'https:';
|
||||
const HTTP = "http:",
|
||||
HTTPS = "https:";
|
||||
|
||||
async function getProxyAgent(opts: ClientOpts) {
|
||||
if (!opts.proxy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {protocol} = url.parse(opts.url);
|
||||
const { protocol } = url.parse(opts.url);
|
||||
|
||||
if (!protocol || ![HTTP, HTTPS].includes(protocol)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const AgentClass = HTTP === protocol
|
||||
? (await import('http-proxy-agent')).HttpProxyAgent
|
||||
: (await import('https-proxy-agent')).HttpsProxyAgent;
|
||||
const AgentClass = HTTP === protocol ? (await import("http-proxy-agent")).HttpProxyAgent : (await import("https-proxy-agent")).HttpsProxyAgent;
|
||||
|
||||
return new AgentClass(opts.proxy);
|
||||
}
|
||||
@@ -211,11 +207,11 @@ async function getClient(opts: ClientOpts): Promise<Client> {
|
||||
// it's not clear how to explicitly configure proxy (as opposed to system proxy),
|
||||
// so in that case, we always use node's modules
|
||||
if (isElectron() && !opts.proxy) {
|
||||
return (await import('electron')).net as Client;
|
||||
return (await import("electron")).net as Client;
|
||||
} else {
|
||||
const {protocol} = url.parse(opts.url);
|
||||
const { protocol } = url.parse(opts.url);
|
||||
|
||||
if (protocol === 'http:' || protocol === 'https:') {
|
||||
if (protocol === "http:" || protocol === "https:") {
|
||||
return await import(protocol.substr(0, protocol.length - 1));
|
||||
} else {
|
||||
throw new Error(`Unrecognized protocol '${protocol}'`);
|
||||
@@ -223,10 +219,13 @@ async function getClient(opts: ClientOpts): Promise<Client> {
|
||||
}
|
||||
}
|
||||
|
||||
function generateError(opts: {
|
||||
method: string;
|
||||
url: string;
|
||||
}, message: string) {
|
||||
function generateError(
|
||||
opts: {
|
||||
method: string;
|
||||
url: string;
|
||||
},
|
||||
message: string
|
||||
) {
|
||||
return new Error(`Request to ${opts.method} ${opts.url} failed, error: ${message}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface ExecOpts {
|
||||
cookieJar?: CookieJar;
|
||||
auth?: {
|
||||
password?: string;
|
||||
},
|
||||
};
|
||||
timeout: number;
|
||||
body?: string | {};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ function protectRevisions(note: BNote) {
|
||||
revision.isProtected = !!note.isProtected;
|
||||
|
||||
// this will force de/encryption
|
||||
revision.setContent(content, {forceSave: true});
|
||||
revision.setContent(content, { forceSave: true });
|
||||
} catch (e) {
|
||||
log.error(`Could not un/protect note revision '${revision.revisionId}'`);
|
||||
|
||||
@@ -33,7 +33,7 @@ function protectRevisions(note: BNote) {
|
||||
const content = attachment.getContent();
|
||||
|
||||
attachment.isProtected = note.isProtected;
|
||||
attachment.setContent(content, {forceSave: true});
|
||||
attachment.setContent(content, { forceSave: true });
|
||||
} catch (e) {
|
||||
log.error(`Could not un/protect attachment '${attachment.attachmentId}'`);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export default function sanitizeAttributeName(origName: string) {
|
||||
const fixedName = (origName === '')
|
||||
? "unnamed"
|
||||
: origName.replace(/[^\p{L}\p{N}_:]/ug, "_");
|
||||
// any not allowed character should be replaced with underscore
|
||||
const fixedName = origName === "" ? "unnamed" : origName.replace(/[^\p{L}\p{N}_:]/gu, "_");
|
||||
// any not allowed character should be replaced with underscore
|
||||
|
||||
return fixedName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import BNote from "../becca/entities/bnote.js";
|
||||
|
||||
function getRunAtHours(note: BNote): number[] {
|
||||
try {
|
||||
return note.getLabelValues('runAtHour').map(hour => parseInt(hour));
|
||||
return note.getLabelValues("runAtHour").map((hour) => parseInt(hour));
|
||||
} catch (e: any) {
|
||||
log.error(`Could not parse runAtHour for note ${note.noteId}: ${e.message}`);
|
||||
|
||||
@@ -21,15 +21,13 @@ function getRunAtHours(note: BNote): number[] {
|
||||
function runNotesWithLabel(runAttrValue: string) {
|
||||
const instanceName = config.General ? config.General.instanceName : null;
|
||||
const currentHours = new Date().getHours();
|
||||
const notes = attributeService.getNotesWithLabel('run', runAttrValue);
|
||||
const notes = attributeService.getNotesWithLabel("run", runAttrValue);
|
||||
|
||||
for (const note of notes) {
|
||||
const runOnInstances = note.getLabelValues('runOnInstance');
|
||||
const runOnInstances = note.getLabelValues("runOnInstance");
|
||||
const runAtHours = getRunAtHours(note);
|
||||
|
||||
if ((runOnInstances.length === 0 || runOnInstances.includes(instanceName))
|
||||
&& (runAtHours.length === 0 || runAtHours.includes(currentHours))
|
||||
) {
|
||||
if ((runOnInstances.length === 0 || runOnInstances.includes(instanceName)) && (runAtHours.length === 0 || runAtHours.includes(currentHours))) {
|
||||
scriptService.executeNoteNoException(note, { originEntity: note });
|
||||
}
|
||||
}
|
||||
@@ -41,13 +39,25 @@ sqlInit.dbReady.then(() => {
|
||||
});
|
||||
|
||||
if (!process.env.TRILIUM_SAFE_MODE) {
|
||||
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
|
||||
setTimeout(
|
||||
cls.wrap(() => runNotesWithLabel("backendStartup")),
|
||||
10 * 1000
|
||||
);
|
||||
|
||||
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
|
||||
setInterval(
|
||||
cls.wrap(() => runNotesWithLabel("hourly")),
|
||||
3600 * 1000
|
||||
);
|
||||
|
||||
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
|
||||
setInterval(
|
||||
cls.wrap(() => runNotesWithLabel("daily")),
|
||||
24 * 3600 * 1000
|
||||
);
|
||||
|
||||
setInterval(cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()), 7 * 3600 * 1000);
|
||||
setInterval(
|
||||
cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()),
|
||||
7 * 3600 * 1000
|
||||
);
|
||||
}
|
||||
|
||||
setInterval(() => protectedSessionService.checkProtectedSessionExpiration(), 30000);
|
||||
|
||||
@@ -3,7 +3,7 @@ import cls from "./cls.js";
|
||||
import log from "./log.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import { ApiParams } from './backend_script_api_interface.js';
|
||||
import { ApiParams } from "./backend_script_api_interface.js";
|
||||
|
||||
interface Bundle {
|
||||
note?: BNote;
|
||||
@@ -17,13 +17,13 @@ interface Bundle {
|
||||
type ScriptParams = any[];
|
||||
|
||||
function executeNote(note: BNote, apiParams: ApiParams) {
|
||||
if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable()) {
|
||||
if (!note.isJavaScript() || note.getScriptEnv() !== "backend" || !note.isContentAvailable()) {
|
||||
log.info(`Cannot execute note ${note.noteId} "${note.title}", note must be of type "Code: JS backend"`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle = getScriptBundle(note, true, 'backend');
|
||||
const bundle = getScriptBundle(note, true, "backend");
|
||||
if (!bundle) {
|
||||
throw new Error("Unable to determine bundle.");
|
||||
}
|
||||
@@ -34,8 +34,7 @@ function executeNote(note: BNote, apiParams: ApiParams) {
|
||||
function executeNoteNoException(note: BNote, apiParams: ApiParams) {
|
||||
try {
|
||||
executeNote(note, apiParams);
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
// just swallow, exception is logged already in executeNote
|
||||
}
|
||||
}
|
||||
@@ -46,10 +45,10 @@ function executeBundle(bundle: Bundle, apiParams: ApiParams = {}) {
|
||||
apiParams.startNote = bundle.note;
|
||||
}
|
||||
|
||||
const originalComponentId = cls.get('componentId');
|
||||
const originalComponentId = cls.get("componentId");
|
||||
|
||||
cls.set('componentId', 'script');
|
||||
cls.set('bundleNoteId', bundle.note?.noteId);
|
||||
cls.set("componentId", "script");
|
||||
cls.set("bundleNoteId", bundle.note?.noteId);
|
||||
|
||||
// last \r\n is necessary if the script contains line comment on its last line
|
||||
const script = `function() {\r
|
||||
@@ -59,14 +58,12 @@ ${bundle.script}\r
|
||||
|
||||
try {
|
||||
return execute(ctx, script);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
log.error(`Execution of script "${bundle.note?.title}" (${bundle.note?.noteId}) failed with error: ${e.message}`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
cls.set('componentId', originalComponentId);
|
||||
} finally {
|
||||
cls.set("componentId", originalComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +87,7 @@ function executeScript(script: string, params: ScriptParams, startNoteId: string
|
||||
// override normal note's content, and it's mime type / script environment
|
||||
const overrideContent = `return (${script}\r\n)(${getParams(params)})`;
|
||||
|
||||
const bundle = getScriptBundle(currentNote, true, 'backend', [], overrideContent);
|
||||
const bundle = getScriptBundle(currentNote, true, "backend", [], overrideContent);
|
||||
if (!bundle) {
|
||||
throw new Error("Unable to determine script bundle.");
|
||||
}
|
||||
@@ -99,7 +96,9 @@ function executeScript(script: string, params: ScriptParams, startNoteId: string
|
||||
}
|
||||
|
||||
function execute(ctx: ScriptContext, script: string) {
|
||||
return function () { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx);
|
||||
return function () {
|
||||
return eval(`const apiContext = this;\r\n(${script}\r\n)()`);
|
||||
}.call(ctx);
|
||||
}
|
||||
|
||||
function getParams(params?: ScriptParams) {
|
||||
@@ -107,14 +106,15 @@ function getParams(params?: ScriptParams) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return params.map(p => {
|
||||
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
|
||||
return p.substr(13);
|
||||
}
|
||||
else {
|
||||
return JSON.stringify(p);
|
||||
}
|
||||
}).join(",");
|
||||
return params
|
||||
.map((p) => {
|
||||
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
|
||||
return p.substr(13);
|
||||
} else {
|
||||
return JSON.stringify(p);
|
||||
}
|
||||
})
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function getScriptBundleForFrontend(note: BNote, script?: string, params?: ScriptParams) {
|
||||
@@ -124,7 +124,7 @@ function getScriptBundleForFrontend(note: BNote, script?: string, params?: Scrip
|
||||
overrideContent = `return (${script}\r\n)(${getParams(params)})`;
|
||||
}
|
||||
|
||||
const bundle = getScriptBundle(note, true, 'frontend', [], overrideContent);
|
||||
const bundle = getScriptBundle(note, true, "frontend", [], overrideContent);
|
||||
|
||||
if (!bundle) {
|
||||
return;
|
||||
@@ -134,7 +134,7 @@ function getScriptBundleForFrontend(note: BNote, script?: string, params?: Scrip
|
||||
bundle.noteId = bundle.note?.noteId;
|
||||
delete bundle.note;
|
||||
|
||||
bundle.allNoteIds = bundle.allNotes?.map(note => note.noteId);
|
||||
bundle.allNoteIds = bundle.allNotes?.map((note) => note.noteId);
|
||||
delete bundle.allNotes;
|
||||
|
||||
return bundle;
|
||||
@@ -149,18 +149,18 @@ function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: string |
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root && note.hasOwnedLabel('disableInclusion')) {
|
||||
if (!root && note.hasOwnedLabel("disableInclusion")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.type !== 'file' && !root && scriptEnv !== note.getScriptEnv()) {
|
||||
if (note.type !== "file" && !root && scriptEnv !== note.getScriptEnv()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle: Bundle = {
|
||||
note: note,
|
||||
script: '',
|
||||
html: '',
|
||||
script: "",
|
||||
html: "",
|
||||
allNotes: [note]
|
||||
};
|
||||
|
||||
@@ -187,24 +187,23 @@ function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: string |
|
||||
}
|
||||
}
|
||||
|
||||
const moduleNoteIds = modules.map(mod => mod.noteId);
|
||||
const moduleNoteIds = modules.map((mod) => mod.noteId);
|
||||
|
||||
// only frontend scripts are async. Backend cannot be async because of transaction management.
|
||||
const isFrontend = scriptEnv === 'frontend';
|
||||
const isFrontend = scriptEnv === "frontend";
|
||||
|
||||
if (note.isJavaScript()) {
|
||||
bundle.script += `
|
||||
apiContext.modules['${note.noteId}'] = { exports: {} };
|
||||
${root ? 'return ' : ''}${isFrontend ? 'await' : ''} ((${isFrontend ? 'async' : ''} function(exports, module, require, api${modules.length > 0 ? ', ' : ''}${modules.map(child => sanitizeVariableName(child.title)).join(', ')}) {
|
||||
${root ? "return " : ""}${isFrontend ? "await" : ""} ((${isFrontend ? "async" : ""} function(exports, module, require, api${modules.length > 0 ? ", " : ""}${modules.map((child) => sanitizeVariableName(child.title)).join(", ")}) {
|
||||
try {
|
||||
${overrideContent || note.getContent()};
|
||||
} catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); }
|
||||
for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
|
||||
return module.exports;
|
||||
}).call({}, {}, apiContext.modules['${note.noteId}'], apiContext.require(${JSON.stringify(moduleNoteIds)}), apiContext.apis['${note.noteId}']${modules.length > 0 ? ', ' : ''}${modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ')}));
|
||||
}).call({}, {}, apiContext.modules['${note.noteId}'], apiContext.require(${JSON.stringify(moduleNoteIds)}), apiContext.apis['${note.noteId}']${modules.length > 0 ? ", " : ""}${modules.map((mod) => `apiContext.modules['${mod.noteId}'].exports`).join(", ")}));
|
||||
`;
|
||||
}
|
||||
else if (note.isHtml()) {
|
||||
} else if (note.isHtml()) {
|
||||
bundle.html += note.getContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toObject } from "./utils.js";
|
||||
import BackendScriptApi from "./backend_script_api.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import { ApiParams } from './backend_script_api_interface.js';
|
||||
import { ApiParams } from "./backend_script_api_interface.js";
|
||||
|
||||
type Module = {
|
||||
exports: any[];
|
||||
@@ -16,22 +16,22 @@ class ScriptContext {
|
||||
constructor(allNotes: BNote[], apiParams: ApiParams) {
|
||||
this.allNotes = allNotes;
|
||||
this.modules = {};
|
||||
this.notes = toObject(allNotes, note => [note.noteId, note]);
|
||||
this.apis = toObject(allNotes, note => [note.noteId, new BackendScriptApi(note, apiParams)]);
|
||||
this.notes = toObject(allNotes, (note) => [note.noteId, note]);
|
||||
this.apis = toObject(allNotes, (note) => [note.noteId, new BackendScriptApi(note, apiParams)]);
|
||||
}
|
||||
|
||||
require(moduleNoteIds: string[]) {
|
||||
return (moduleName: string) => {
|
||||
const candidates = this.allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
const note = candidates.find(c => c.title === moduleName);
|
||||
const candidates = this.allNotes.filter((note) => moduleNoteIds.includes(note.noteId));
|
||||
const note = candidates.find((c) => c.title === moduleName);
|
||||
|
||||
if (!note) {
|
||||
return require(moduleName);
|
||||
}
|
||||
|
||||
return this.modules[note.noteId].exports;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ScriptContext;
|
||||
|
||||
@@ -7,10 +7,9 @@ import becca from "../../../becca/becca.js";
|
||||
import SearchContext from "../search_context.js";
|
||||
|
||||
class AncestorExp extends Expression {
|
||||
|
||||
private ancestorNoteId: string;
|
||||
private ancestorDepthComparator;
|
||||
|
||||
|
||||
ancestorDepth?: string;
|
||||
|
||||
constructor(ancestorNoteId: string, ancestorDepth?: string) {
|
||||
@@ -59,15 +58,12 @@ class AncestorExp extends Expression {
|
||||
const comparedDepth = parseInt(depthCondition.substr(2));
|
||||
|
||||
if (depthCondition.startsWith("eq")) {
|
||||
return depth => depth === comparedDepth;
|
||||
}
|
||||
else if (depthCondition.startsWith("gt")) {
|
||||
return depth => depth > comparedDepth;
|
||||
}
|
||||
else if (depthCondition.startsWith("lt")) {
|
||||
return depth => depth < comparedDepth;
|
||||
}
|
||||
else {
|
||||
return (depth) => depth === comparedDepth;
|
||||
} else if (depthCondition.startsWith("gt")) {
|
||||
return (depth) => depth > comparedDepth;
|
||||
} else if (depthCondition.startsWith("lt")) {
|
||||
return (depth) => depth < comparedDepth;
|
||||
} else {
|
||||
log.error(`Unrecognized depth condition value ${depthCondition}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import becca from "../../../becca/becca.js";
|
||||
import Expression from "./expression.js";
|
||||
|
||||
class AttributeExistsExp extends Expression {
|
||||
|
||||
private attributeType: string;
|
||||
private attributeName: string;
|
||||
private isTemplateLabel: boolean;
|
||||
@@ -19,14 +18,12 @@ class AttributeExistsExp extends Expression {
|
||||
this.attributeType = attributeType;
|
||||
this.attributeName = attributeName;
|
||||
// template attr is used as a marker for templates, but it's not meant to be inherited
|
||||
this.isTemplateLabel = this.attributeType === 'label' && (this.attributeName === 'template' || this.attributeName === 'workspacetemplate');
|
||||
this.isTemplateLabel = this.attributeType === "label" && (this.attributeName === "template" || this.attributeName === "workspacetemplate");
|
||||
this.prefixMatch = prefixMatch;
|
||||
}
|
||||
|
||||
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
|
||||
const attrs = this.prefixMatch
|
||||
? becca.findAttributesWithPrefix(this.attributeType, this.attributeName)
|
||||
: becca.findAttributes(this.attributeType, this.attributeName);
|
||||
const attrs = this.prefixMatch ? becca.findAttributesWithPrefix(this.attributeType, this.attributeName) : becca.findAttributes(this.attributeType, this.attributeName);
|
||||
|
||||
const resultNoteSet = new NoteSet();
|
||||
|
||||
@@ -35,11 +32,9 @@ class AttributeExistsExp extends Expression {
|
||||
|
||||
if (attr.isInheritable && !this.isTemplateLabel) {
|
||||
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
else if (note.isInherited() && !this.isTemplateLabel) {
|
||||
} else if (note.isInherited() && !this.isTemplateLabel) {
|
||||
resultNoteSet.addAll(note.getInheritingNotes());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
resultNoteSet.add(note);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import NoteSet from "../note_set.js";
|
||||
import SearchContext from "../search_context.js";
|
||||
|
||||
class ChildOfExp extends Expression {
|
||||
|
||||
private subExpression: Expression;
|
||||
|
||||
constructor(subExpression: Expression) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import SearchContext from "../search_context.js";
|
||||
type Comparator = (value: string) => boolean;
|
||||
|
||||
class LabelComparisonExp extends Expression {
|
||||
|
||||
private attributeType: string;
|
||||
private attributeName: string;
|
||||
private comparator: Comparator;
|
||||
@@ -32,11 +31,9 @@ class LabelComparisonExp extends Expression {
|
||||
if (inputNoteSet.hasNoteId(note.noteId) && this.comparator(value)) {
|
||||
if (attr.isInheritable) {
|
||||
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
else if (note.isInherited()) {
|
||||
} else if (note.isInherited()) {
|
||||
resultNoteSet.addAll(note.getInheritingNotes());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
resultNoteSet.add(note);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ import striptags from "striptags";
|
||||
import { normalize } from "../../utils.js";
|
||||
import sql from "../../sql.js";
|
||||
|
||||
|
||||
const ALLOWED_OPERATORS = new Set(['=', '!=', '*=*', '*=', '=*', '%=']);
|
||||
const ALLOWED_OPERATORS = new Set(["=", "!=", "*=*", "*=", "=*", "%="]);
|
||||
|
||||
const cachedRegexes: Record<string, RegExp> = {};
|
||||
|
||||
function getRegex(str: string): RegExp {
|
||||
if (!(str in cachedRegexes)) {
|
||||
cachedRegexes[str] = new RegExp(str, 'ms'); // multiline, dot-all
|
||||
cachedRegexes[str] = new RegExp(str, "ms"); // multiline, dot-all
|
||||
}
|
||||
|
||||
return cachedRegexes[str];
|
||||
@@ -34,13 +33,12 @@ interface ConstructorOpts {
|
||||
type SearchRow = Pick<NoteRow, "noteId" | "type" | "mime" | "content" | "isProtected">;
|
||||
|
||||
class NoteContentFulltextExp extends Expression {
|
||||
|
||||
private operator: string;
|
||||
private tokens: string[];
|
||||
private raw: boolean;
|
||||
private flatText: boolean;
|
||||
|
||||
constructor(operator: string, {tokens, raw, flatText}: ConstructorOpts) {
|
||||
|
||||
constructor(operator: string, { tokens, raw, flatText }: ConstructorOpts) {
|
||||
super();
|
||||
|
||||
this.operator = operator;
|
||||
@@ -57,19 +55,18 @@ class NoteContentFulltextExp extends Expression {
|
||||
}
|
||||
|
||||
const resultNoteSet = new NoteSet();
|
||||
|
||||
|
||||
for (const row of sql.iterateRows<SearchRow>(`
|
||||
SELECT noteId, type, mime, content, isProtected
|
||||
FROM notes JOIN blobs USING (blobId)
|
||||
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap') AND isDeleted = 0`)) {
|
||||
|
||||
this.findInText(row, inputNoteSet, resultNoteSet);
|
||||
}
|
||||
|
||||
return resultNoteSet;
|
||||
}
|
||||
|
||||
findInText({noteId, isProtected, content, type, mime}: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
|
||||
findInText({ noteId, isProtected, content, type, mime }: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
|
||||
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
|
||||
return;
|
||||
}
|
||||
@@ -96,24 +93,23 @@ class NoteContentFulltextExp extends Expression {
|
||||
if (this.tokens.length === 1) {
|
||||
const [token] = this.tokens;
|
||||
|
||||
if ((this.operator === '=' && token === content)
|
||||
|| (this.operator === '!=' && token !== content)
|
||||
|| (this.operator === '*=' && content.endsWith(token))
|
||||
|| (this.operator === '=*' && content.startsWith(token))
|
||||
|| (this.operator === '*=*' && content.includes(token))
|
||||
|| (this.operator === '%=' && getRegex(token).test(content))) {
|
||||
|
||||
if (
|
||||
(this.operator === "=" && token === content) ||
|
||||
(this.operator === "!=" && token !== content) ||
|
||||
(this.operator === "*=" && content.endsWith(token)) ||
|
||||
(this.operator === "=*" && content.startsWith(token)) ||
|
||||
(this.operator === "*=*" && content.includes(token)) ||
|
||||
(this.operator === "%=" && getRegex(token).test(content))
|
||||
) {
|
||||
resultNoteSet.add(becca.notes[noteId]);
|
||||
}
|
||||
} else {
|
||||
const nonMatchingToken = this.tokens.find(token =>
|
||||
!content?.includes(token) &&
|
||||
(
|
||||
const nonMatchingToken = this.tokens.find(
|
||||
(token) =>
|
||||
!content?.includes(token) &&
|
||||
// in case of default fulltext search, we should consider both title, attrs and content
|
||||
// so e.g. "hello world" should match when "hello" is in title and "world" in content
|
||||
!this.flatText
|
||||
|| !becca.notes[noteId].getFlatText().includes(token)
|
||||
)
|
||||
(!this.flatText || !becca.notes[noteId].getFlatText().includes(token))
|
||||
);
|
||||
|
||||
if (!nonMatchingToken) {
|
||||
@@ -127,18 +123,17 @@ class NoteContentFulltextExp extends Expression {
|
||||
preprocessContent(content: string | Buffer, type: string, mime: string) {
|
||||
content = normalize(content.toString());
|
||||
|
||||
if (type === 'text' && mime === 'text/html') {
|
||||
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
|
||||
if (type === "text" && mime === "text/html") {
|
||||
if (!this.raw && content.length < 20000) {
|
||||
// striptags is slow for very large notes
|
||||
content = this.stripTags(content);
|
||||
}
|
||||
|
||||
content = content.replace(/ /g, ' ');
|
||||
}
|
||||
else if (type === 'mindMap' && mime === 'application/json') {
|
||||
|
||||
let mindMapcontent = JSON.parse (content);
|
||||
content = content.replace(/ /g, " ");
|
||||
} else if (type === "mindMap" && mime === "application/json") {
|
||||
let mindMapcontent = JSON.parse(content);
|
||||
|
||||
// Define interfaces for the JSON structure
|
||||
// Define interfaces for the JSON structure
|
||||
interface MindmapNode {
|
||||
id: string;
|
||||
topic: string;
|
||||
@@ -146,62 +141,58 @@ class NoteContentFulltextExp extends Expression {
|
||||
direction?: number;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
|
||||
interface MindmapData {
|
||||
nodedata: MindmapNode;
|
||||
arrows: any[]; // If you know the structure, replace `any` with the correct type
|
||||
summaries: any[];
|
||||
direction: number;
|
||||
theme: {
|
||||
name: string;
|
||||
type: string;
|
||||
palette: string[];
|
||||
cssvar: Record<string, string>; // Object with string keys and string values
|
||||
name: string;
|
||||
type: string;
|
||||
palette: string[];
|
||||
cssvar: Record<string, string>; // Object with string keys and string values
|
||||
};
|
||||
}
|
||||
|
||||
// Recursive function to collect all topics
|
||||
function collectTopics(node: MindmapNode): string[] {
|
||||
|
||||
// Recursive function to collect all topics
|
||||
function collectTopics(node: MindmapNode): string[] {
|
||||
// Collect the current node's topic
|
||||
let topics = [node.topic];
|
||||
|
||||
|
||||
// If the node has children, collect topics recursively
|
||||
if (node.children && node.children.length > 0) {
|
||||
for (const child of node.children) {
|
||||
topics = topics.concat(collectTopics(child));
|
||||
for (const child of node.children) {
|
||||
topics = topics.concat(collectTopics(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return topics;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Start extracting from the root node
|
||||
const topicsArray = collectTopics(mindMapcontent.nodedata);
|
||||
|
||||
|
||||
// Combine topics into a single string
|
||||
const topicsString = topicsArray.join(", ");
|
||||
|
||||
|
||||
|
||||
content = normalize(topicsString.toString());
|
||||
}
|
||||
else if (type === 'canvas' && mime === 'application/json') {
|
||||
} else if (type === "canvas" && mime === "application/json") {
|
||||
interface Element {
|
||||
type: string;
|
||||
text?: string; // Optional since not all objects have a `text` property
|
||||
id: string;
|
||||
[key: string]: any; // Other properties that may exist
|
||||
}
|
||||
|
||||
let canvasContent = JSON.parse (content);
|
||||
const elements: Element [] = canvasContent.elements;
|
||||
|
||||
let canvasContent = JSON.parse(content);
|
||||
const elements: Element[] = canvasContent.elements;
|
||||
const texts = elements
|
||||
.filter((element: Element) => element.type === 'text' && element.text) // Filter for 'text' type elements with a 'text' property
|
||||
.filter((element: Element) => element.type === "text" && element.text) // Filter for 'text' type elements with a 'text' property
|
||||
.map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering
|
||||
|
||||
content = normalize(texts.toString())
|
||||
}
|
||||
|
||||
content = normalize(texts.toString());
|
||||
}
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
@@ -210,17 +201,17 @@ class NoteContentFulltextExp extends Expression {
|
||||
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
|
||||
// we want to insert space in place of block tags (because they imply text separation)
|
||||
// but we don't want to insert text for typical formatting inline tags which can occur within one word
|
||||
const linkTag = 'a';
|
||||
const inlineFormattingTags = ['b', 'strong', 'em', 'i', 'span', 'big', 'small', 'font', 'sub', 'sup'];
|
||||
const linkTag = "a";
|
||||
const inlineFormattingTags = ["b", "strong", "em", "i", "span", "big", "small", "font", "sub", "sup"];
|
||||
|
||||
// replace tags which imply text separation with a space
|
||||
content = striptags(content, [linkTag, ...inlineFormattingTags], ' ');
|
||||
content = striptags(content, [linkTag, ...inlineFormattingTags], " ");
|
||||
|
||||
// replace the inline formatting tags (but not links) without a space
|
||||
content = striptags(content, [linkTag], '');
|
||||
content = striptags(content, [linkTag], "");
|
||||
|
||||
// at least the closing link tag can be easily stripped
|
||||
return content.replace(/<\/a>/ig, "");
|
||||
return content.replace(/<\/a>/gi, "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class NoteFlatTextExp extends Expression {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.parents.length === 0 || note.noteId === 'root') {
|
||||
if (note.parents.length === 0 || note.noteId === "root") {
|
||||
// we've reached root, but there are still remaining tokens -> this candidate note produced no result
|
||||
return;
|
||||
}
|
||||
@@ -82,15 +82,14 @@ class NoteFlatTextExp extends Expression {
|
||||
}
|
||||
|
||||
if (foundTokens.length > 0) {
|
||||
const newRemainingTokens = remainingTokens.filter(token => !foundTokens.includes(token));
|
||||
const newRemainingTokens = remainingTokens.filter((token) => !foundTokens.includes(token));
|
||||
|
||||
searchPathTowardsRoot(parentNote, newRemainingTokens, [note.noteId, ...takenPath]);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId, ...takenPath]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const candidateNotes = this.getCandidateNotes(inputNoteSet);
|
||||
|
||||
@@ -109,9 +108,7 @@ class NoteFlatTextExp extends Expression {
|
||||
}
|
||||
|
||||
for (const attribute of note.ownedAttributes) {
|
||||
if (normalize(attribute.name).includes(token)
|
||||
|| normalize(attribute.value).includes(token)) {
|
||||
|
||||
if (normalize(attribute.name).includes(token) || normalize(attribute.value).includes(token)) {
|
||||
foundAttrTokens.push(token);
|
||||
}
|
||||
}
|
||||
@@ -128,7 +125,7 @@ class NoteFlatTextExp extends Expression {
|
||||
}
|
||||
|
||||
if (foundTokens.length > 0) {
|
||||
const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
|
||||
const remainingTokens = this.tokens.filter((token) => !foundTokens.includes(token));
|
||||
|
||||
searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId]);
|
||||
}
|
||||
|
||||
@@ -9,15 +9,13 @@ class OrExp extends Expression {
|
||||
private subExpressions: Expression[];
|
||||
|
||||
static of(subExpressions: Expression[]) {
|
||||
subExpressions = subExpressions.filter(exp => !!exp);
|
||||
subExpressions = subExpressions.filter((exp) => !!exp);
|
||||
|
||||
if (subExpressions.length === 1) {
|
||||
return subExpressions[0];
|
||||
}
|
||||
else if (subExpressions.length > 0) {
|
||||
} else if (subExpressions.length > 0) {
|
||||
return new OrExp(subExpressions);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return new TrueExp();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ interface OrderDefinition {
|
||||
}
|
||||
|
||||
class OrderByAndLimitExp extends Expression {
|
||||
|
||||
private orderDefinitions: OrderDefinition[];
|
||||
private limit: number;
|
||||
subExpression: Expression | null;
|
||||
@@ -42,10 +41,10 @@ class OrderByAndLimitExp extends Expression {
|
||||
throw new Error("Missing subexpression");
|
||||
}
|
||||
|
||||
let {notes} = this.subExpression.execute(inputNoteSet, executionContext, searchContext);
|
||||
let { notes } = this.subExpression.execute(inputNoteSet, executionContext, searchContext);
|
||||
|
||||
notes.sort((a, b) => {
|
||||
for (const {valueExtractor, smaller, larger} of this.orderDefinitions) {
|
||||
for (const { valueExtractor, smaller, larger } of this.orderDefinitions) {
|
||||
let valA: string | number | Date | null = valueExtractor.extract(a);
|
||||
let valB: string | number | Date | null = valueExtractor.extract(b);
|
||||
|
||||
@@ -60,25 +59,20 @@ class OrderByAndLimitExp extends Expression {
|
||||
if (valA === null && valB === null) {
|
||||
// neither has attribute at all
|
||||
continue;
|
||||
}
|
||||
else if (valB === null) {
|
||||
} else if (valB === null) {
|
||||
return smaller;
|
||||
}
|
||||
else if (valA === null) {
|
||||
} else if (valA === null) {
|
||||
return larger;
|
||||
}
|
||||
|
||||
|
||||
// if both are dates, then parse them for dates comparison
|
||||
if (typeof valA === "string" && this.isDate(valA) &&
|
||||
typeof valB === "string" && this.isDate(valB)) {
|
||||
if (typeof valA === "string" && this.isDate(valA) && typeof valB === "string" && this.isDate(valB)) {
|
||||
valA = new Date(valA);
|
||||
valB = new Date(valB);
|
||||
}
|
||||
|
||||
// if both are numbers, then parse them for numerical comparison
|
||||
else if (typeof valA === "string" && this.isNumber(valA) &&
|
||||
typeof valB === "string" && this.isNumber(valB)) {
|
||||
else if (typeof valA === "string" && this.isNumber(valA) && typeof valB === "string" && this.isNumber(valB)) {
|
||||
valA = parseFloat(valA);
|
||||
valB = parseFloat(valB);
|
||||
}
|
||||
@@ -108,13 +102,13 @@ class OrderByAndLimitExp extends Expression {
|
||||
}
|
||||
|
||||
isDate(date: number | string) {
|
||||
return !isNaN(new Date(date).getTime());
|
||||
return !isNaN(new Date(date).getTime());
|
||||
}
|
||||
|
||||
isNumber(x: number | string) {
|
||||
if (typeof x === 'number') {
|
||||
if (typeof x === "number") {
|
||||
return true;
|
||||
} else if (typeof x === 'string') {
|
||||
} else if (typeof x === "string") {
|
||||
// isNaN will return false for blank string
|
||||
return x.trim() !== "" && !isNaN(parseInt(x, 10));
|
||||
} else {
|
||||
|
||||
@@ -9,31 +9,31 @@ import buildComparator from "../services/build_comparator.js";
|
||||
* we need the case-sensitive form, so we have this translation object.
|
||||
*/
|
||||
const PROP_MAPPING: Record<string, string> = {
|
||||
"noteid": "noteId",
|
||||
"title": "title",
|
||||
"type": "type",
|
||||
"mime": "mime",
|
||||
"isprotected": "isProtected",
|
||||
"isarchived": "isArchived",
|
||||
"datecreated": "dateCreated",
|
||||
"datemodified": "dateModified",
|
||||
"utcdatecreated": "utcDateCreated",
|
||||
"utcdatemodified": "utcDateModified",
|
||||
"parentcount": "parentCount",
|
||||
"childrencount": "childrenCount",
|
||||
"attributecount": "attributeCount",
|
||||
"labelcount": "labelCount",
|
||||
"ownedlabelcount": "ownedLabelCount",
|
||||
"relationcount": "relationCount",
|
||||
"ownedrelationcount": "ownedRelationCount",
|
||||
"relationcountincludinglinks": "relationCountIncludingLinks",
|
||||
"ownedrelationcountincludinglinks": "ownedRelationCountIncludingLinks",
|
||||
"targetrelationcount": "targetRelationCount",
|
||||
"targetrelationcountincludinglinks": "targetRelationCountIncludingLinks",
|
||||
"contentsize": "contentSize",
|
||||
"contentandattachmentssize": "contentAndAttachmentsSize",
|
||||
"contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize",
|
||||
"revisioncount": "revisionCount"
|
||||
noteid: "noteId",
|
||||
title: "title",
|
||||
type: "type",
|
||||
mime: "mime",
|
||||
isprotected: "isProtected",
|
||||
isarchived: "isArchived",
|
||||
datecreated: "dateCreated",
|
||||
datemodified: "dateModified",
|
||||
utcdatecreated: "utcDateCreated",
|
||||
utcdatemodified: "utcDateModified",
|
||||
parentcount: "parentCount",
|
||||
childrencount: "childrenCount",
|
||||
attributecount: "attributeCount",
|
||||
labelcount: "labelCount",
|
||||
ownedlabelcount: "ownedLabelCount",
|
||||
relationcount: "relationCount",
|
||||
ownedrelationcount: "ownedRelationCount",
|
||||
relationcountincludinglinks: "relationCountIncludingLinks",
|
||||
ownedrelationcountincludinglinks: "ownedRelationCountIncludingLinks",
|
||||
targetrelationcount: "targetRelationCount",
|
||||
targetrelationcountincludinglinks: "targetRelationCountIncludingLinks",
|
||||
contentsize: "contentSize",
|
||||
contentandattachmentssize: "contentAndAttachmentsSize",
|
||||
contentandattachmentsandrevisionssize: "contentAndAttachmentsAndRevisionsSize",
|
||||
revisioncount: "revisionCount"
|
||||
};
|
||||
|
||||
interface SearchContext {
|
||||
@@ -41,7 +41,6 @@ interface SearchContext {
|
||||
}
|
||||
|
||||
class PropertyComparisonExp extends Expression {
|
||||
|
||||
private propertyName: string;
|
||||
private operator: string;
|
||||
private comparedValue: string;
|
||||
@@ -59,7 +58,7 @@ class PropertyComparisonExp extends Expression {
|
||||
this.comparedValue = comparedValue; // for DEBUG mode
|
||||
this.comparator = buildComparator(operator, comparedValue);
|
||||
|
||||
if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyName)) {
|
||||
if (["contentsize", "contentandattachmentssize", "contentandattachmentsandrevisionssize", "revisioncount"].includes(this.propertyName)) {
|
||||
searchContext.dbLoadNeeded = true;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +69,7 @@ class PropertyComparisonExp extends Expression {
|
||||
for (const note of inputNoteSet.notes) {
|
||||
let value = (note as any)[this.propertyName];
|
||||
|
||||
if (value !== undefined && value !== null && typeof value !== 'string') {
|
||||
if (value !== undefined && value !== null && typeof value !== "string") {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class RelationWhereExp extends Expression {
|
||||
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
|
||||
const candidateNoteSet = new NoteSet();
|
||||
|
||||
for (const attr of becca.findAttributes('relation', this.relationName)) {
|
||||
for (const attr of becca.findAttributes("relation", this.relationName)) {
|
||||
const note = attr.note;
|
||||
|
||||
if (inputNoteSet.hasNoteId(note.noteId) && attr.targetNote) {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import BNote from "../../becca/entities/bnote.js";
|
||||
|
||||
class NoteSet {
|
||||
|
||||
private noteIdSet: Set<string>;
|
||||
|
||||
notes: BNote[];
|
||||
@@ -11,7 +10,7 @@ class NoteSet {
|
||||
|
||||
constructor(notes: BNote[] = []) {
|
||||
this.notes = notes;
|
||||
this.noteIdSet = new Set(notes.map(note => note.noteId));
|
||||
this.noteIdSet = new Set(notes.map((note) => note.noteId));
|
||||
this.sorted = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
import hoistedNoteService from "../hoisted_note.js";
|
||||
import { SearchParams } from './services/types.js';
|
||||
import { SearchParams } from "./services/types.js";
|
||||
|
||||
class SearchContext {
|
||||
|
||||
fastSearch: boolean;
|
||||
includeArchivedNotes: boolean;
|
||||
includeHiddenNotes: boolean;
|
||||
|
||||
@@ -16,7 +16,7 @@ class SearchResult {
|
||||
}
|
||||
|
||||
get notePath() {
|
||||
return this.notePathArray.join('/');
|
||||
return this.notePathArray.join("/");
|
||||
}
|
||||
|
||||
get noteId() {
|
||||
@@ -38,18 +38,14 @@ class SearchResult {
|
||||
// Title matching scores, make sure to always win
|
||||
if (normalizedTitle === normalizedQuery) {
|
||||
this.score += 2000; // Increased from 1000 to ensure exact matches always win
|
||||
}
|
||||
else if (normalizedTitle.startsWith(normalizedQuery)) {
|
||||
this.score += 500; // Increased to give more weight to prefix matches
|
||||
}
|
||||
else if (normalizedTitle.includes(` ${normalizedQuery} `) ||
|
||||
normalizedTitle.startsWith(`${normalizedQuery} `) ||
|
||||
normalizedTitle.endsWith(` ${normalizedQuery}`)) {
|
||||
this.score += 300; // Increased to better distinguish word matches
|
||||
} else if (normalizedTitle.startsWith(normalizedQuery)) {
|
||||
this.score += 500; // Increased to give more weight to prefix matches
|
||||
} else if (normalizedTitle.includes(` ${normalizedQuery} `) || normalizedTitle.startsWith(`${normalizedQuery} `) || normalizedTitle.endsWith(` ${normalizedQuery}`)) {
|
||||
this.score += 300; // Increased to better distinguish word matches
|
||||
}
|
||||
|
||||
// Add scores for partial matches with adjusted weights
|
||||
this.addScoreForStrings(tokens, note.title, 2.0); // Increased to give more weight to title matches
|
||||
this.addScoreForStrings(tokens, note.title, 2.0); // Increased to give more weight to title matches
|
||||
this.addScoreForStrings(tokens, this.notePathTitle, 0.3); // Reduced to further de-emphasize path matches
|
||||
|
||||
if (note.isInHiddenSubtree()) {
|
||||
|
||||
@@ -8,26 +8,26 @@ function getRegex(str: string) {
|
||||
return cachedRegexes[str];
|
||||
}
|
||||
|
||||
type Comparator<T> = (comparedValue: T) => ((val: string) => boolean);
|
||||
type Comparator<T> = (comparedValue: T) => (val: string) => boolean;
|
||||
|
||||
const stringComparators: Record<string, Comparator<string>> = {
|
||||
"=": comparedValue => (val => val === comparedValue),
|
||||
"!=": comparedValue => (val => val !== comparedValue),
|
||||
">": comparedValue => (val => val > comparedValue),
|
||||
">=": comparedValue => (val => val >= comparedValue),
|
||||
"<": comparedValue => (val => val < comparedValue),
|
||||
"<=": comparedValue => (val => val <= comparedValue),
|
||||
"*=": comparedValue => (val => !!val && val.endsWith(comparedValue)),
|
||||
"=*": comparedValue => (val => !!val && val.startsWith(comparedValue)),
|
||||
"*=*": comparedValue => (val => !!val && val.includes(comparedValue)),
|
||||
"%=": comparedValue => (val => !!val && !!getRegex(comparedValue).test(val)),
|
||||
"=": (comparedValue) => (val) => val === comparedValue,
|
||||
"!=": (comparedValue) => (val) => val !== comparedValue,
|
||||
">": (comparedValue) => (val) => val > comparedValue,
|
||||
">=": (comparedValue) => (val) => val >= comparedValue,
|
||||
"<": (comparedValue) => (val) => val < comparedValue,
|
||||
"<=": (comparedValue) => (val) => val <= comparedValue,
|
||||
"*=": (comparedValue) => (val) => !!val && val.endsWith(comparedValue),
|
||||
"=*": (comparedValue) => (val) => !!val && val.startsWith(comparedValue),
|
||||
"*=*": (comparedValue) => (val) => !!val && val.includes(comparedValue),
|
||||
"%=": (comparedValue) => (val) => !!val && !!getRegex(comparedValue).test(val)
|
||||
};
|
||||
|
||||
const numericComparators: Record<string, Comparator<number>> = {
|
||||
">": comparedValue => (val => parseFloat(val) > comparedValue),
|
||||
">=": comparedValue => (val => parseFloat(val) >= comparedValue),
|
||||
"<": comparedValue => (val => parseFloat(val) < comparedValue),
|
||||
"<=": comparedValue => (val => parseFloat(val) <= comparedValue)
|
||||
">": (comparedValue) => (val) => parseFloat(val) > comparedValue,
|
||||
">=": (comparedValue) => (val) => parseFloat(val) >= comparedValue,
|
||||
"<": (comparedValue) => (val) => parseFloat(val) < comparedValue,
|
||||
"<=": (comparedValue) => (val) => parseFloat(val) <= comparedValue
|
||||
};
|
||||
|
||||
function buildComparator(operator: string, comparedValue: string) {
|
||||
|
||||
@@ -9,28 +9,28 @@ function handleParens(tokens: TokenStructure) {
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const leftIdx = tokens.findIndex(token => "token" in token && token.token === '(');
|
||||
const leftIdx = tokens.findIndex((token) => "token" in token && token.token === "(");
|
||||
|
||||
if (leftIdx === -1) {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
let rightIdx;
|
||||
let parensLevel = 0
|
||||
let parensLevel = 0;
|
||||
|
||||
for (rightIdx = leftIdx; rightIdx < tokens.length; rightIdx++) {
|
||||
const token = tokens[rightIdx];
|
||||
if (!("token" in token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.token === ')') {
|
||||
|
||||
if (token.token === ")") {
|
||||
parensLevel--;
|
||||
|
||||
if (parensLevel === 0) {
|
||||
break;
|
||||
}
|
||||
} else if (token.token === '(') {
|
||||
} else if (token.token === "(") {
|
||||
parensLevel++;
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,7 @@ function handleParens(tokens: TokenStructure) {
|
||||
throw new Error("Did not find matching right parenthesis.");
|
||||
}
|
||||
|
||||
tokens = [
|
||||
...tokens.slice(0, leftIdx),
|
||||
handleParens(tokens.slice(leftIdx + 1, rightIdx)),
|
||||
...tokens.slice(rightIdx + 1)
|
||||
] as (TokenData | TokenData[])[];
|
||||
tokens = [...tokens.slice(0, leftIdx), handleParens(tokens.slice(leftIdx + 1, rightIdx)), ...tokens.slice(rightIdx + 1)] as (TokenData | TokenData[])[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,23 +9,22 @@ function lex(str: string) {
|
||||
|
||||
let quotes: boolean | string = false; // otherwise contains used quote - ', " or `
|
||||
let fulltextEnded = false;
|
||||
let currentWord = '';
|
||||
let currentWord = "";
|
||||
|
||||
function isSymbolAnOperator(chr: string) {
|
||||
return ['=', '*', '>', '<', '!', "-", "+", '%', ','].includes(chr);
|
||||
return ["=", "*", ">", "<", "!", "-", "+", "%", ","].includes(chr);
|
||||
}
|
||||
|
||||
function isPreviousSymbolAnOperator() {
|
||||
if (currentWord.length === 0) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return isSymbolAnOperator(currentWord[currentWord.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
function finishWord(endIndex: number, createAlsoForEmptyWords = false) {
|
||||
if (currentWord === '' && !createAlsoForEmptyWords) {
|
||||
if (currentWord === "" && !createAlsoForEmptyWords) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,84 +43,70 @@ function lex(str: string) {
|
||||
fulltextQuery = str.substr(0, endIndex + 1);
|
||||
}
|
||||
|
||||
currentWord = '';
|
||||
currentWord = "";
|
||||
}
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const chr = str[i];
|
||||
|
||||
if (chr === '\\') {
|
||||
if (chr === "\\") {
|
||||
if (i + 1 < str.length) {
|
||||
i++;
|
||||
|
||||
currentWord += str[i];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
currentWord += chr;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (['"', "'", '`'].includes(chr)) {
|
||||
} else if (['"', "'", "`"].includes(chr)) {
|
||||
if (!quotes) {
|
||||
if (currentWord.length === 0 || isPreviousSymbolAnOperator()) {
|
||||
finishWord(i - 1);
|
||||
|
||||
quotes = chr;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// quote inside a word does not have special meening and does not break word
|
||||
// e.g. d'Artagnan is kept as a single token
|
||||
currentWord += chr;
|
||||
}
|
||||
}
|
||||
else if (quotes === chr) {
|
||||
} else if (quotes === chr) {
|
||||
finishWord(i - 1, true);
|
||||
|
||||
quotes = false;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// it's a quote, but within other kind of quotes, so it's valid as a literal character
|
||||
currentWord += chr;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (!quotes) {
|
||||
if (!fulltextEnded && currentWord === 'note' && chr === '.' && i + 1 < str.length) {
|
||||
} else if (!quotes) {
|
||||
if (!fulltextEnded && currentWord === "note" && chr === "." && i + 1 < str.length) {
|
||||
fulltextEnded = true;
|
||||
}
|
||||
|
||||
if (chr === '#' || chr === '~') {
|
||||
if (chr === "#" || chr === "~") {
|
||||
if (!fulltextEnded) {
|
||||
fulltextEnded = true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
finishWord(i - 1);
|
||||
}
|
||||
|
||||
currentWord = chr;
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (['#', '~'].includes(currentWord) && chr === '!') {
|
||||
} else if (["#", "~"].includes(currentWord) && chr === "!") {
|
||||
currentWord += chr;
|
||||
continue;
|
||||
}
|
||||
else if (chr === ' ') {
|
||||
} else if (chr === " ") {
|
||||
finishWord(i - 1);
|
||||
continue;
|
||||
}
|
||||
else if (fulltextEnded && ['(', ')', '.'].includes(chr)) {
|
||||
} else if (fulltextEnded && ["(", ")", "."].includes(chr)) {
|
||||
finishWord(i - 1);
|
||||
currentWord += chr;
|
||||
finishWord(i);
|
||||
continue;
|
||||
}
|
||||
else if (fulltextEnded
|
||||
&& !['#!', '~!'].includes(currentWord)
|
||||
&& isPreviousSymbolAnOperator() !== isSymbolAnOperator(chr)) {
|
||||
|
||||
} else if (fulltextEnded && !["#!", "~!"].includes(currentWord) && isPreviousSymbolAnOperator() !== isSymbolAnOperator(chr)) {
|
||||
finishWord(i - 1);
|
||||
|
||||
currentWord += chr;
|
||||
@@ -129,7 +114,7 @@ function lex(str: string) {
|
||||
}
|
||||
}
|
||||
|
||||
if (chr === ',') {
|
||||
if (chr === ",") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -144,7 +129,7 @@ function lex(str: string) {
|
||||
fulltextQuery,
|
||||
fulltextTokens,
|
||||
expressionTokens
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default lex;
|
||||
|
||||
@@ -25,7 +25,7 @@ import { TokenData, TokenStructure } from "./types.js";
|
||||
import Expression from "../expressions/expression.js";
|
||||
|
||||
function getFulltext(_tokens: TokenData[], searchContext: SearchContext) {
|
||||
const tokens: string[] = _tokens.map(t => removeDiacritic(t.token));
|
||||
const tokens: string[] = _tokens.map((t) => removeDiacritic(t.token));
|
||||
|
||||
searchContext.highlightedTokens.push(...tokens);
|
||||
|
||||
@@ -34,28 +34,13 @@ function getFulltext(_tokens: TokenData[], searchContext: SearchContext) {
|
||||
}
|
||||
|
||||
if (!searchContext.fastSearch) {
|
||||
return new OrExp([
|
||||
new NoteFlatTextExp(tokens),
|
||||
new NoteContentFulltextExp('*=*', {tokens, flatText: true})
|
||||
]);
|
||||
}
|
||||
else {
|
||||
return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp("*=*", { tokens, flatText: true })]);
|
||||
} else {
|
||||
return new NoteFlatTextExp(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
const OPERATORS = new Set([
|
||||
"=",
|
||||
"!=",
|
||||
"*=*",
|
||||
"*=",
|
||||
"=*",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"%="
|
||||
]);
|
||||
const OPERATORS = new Set(["=", "!=", "*=*", "*=", "=*", ">", ">=", "<", "<=", "%="]);
|
||||
|
||||
function isOperator(token: TokenData) {
|
||||
if (Array.isArray(token)) {
|
||||
@@ -76,7 +61,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
let i: number;
|
||||
|
||||
function context(i: number) {
|
||||
let {startIndex, endIndex} = tokens[i];
|
||||
let { startIndex, endIndex } = tokens[i];
|
||||
startIndex = Math.max(0, (startIndex || 0) - 20);
|
||||
endIndex = Math.min(searchContext.originalQuery.length, (endIndex || Number.MAX_SAFE_INTEGER) + 20);
|
||||
|
||||
@@ -86,8 +71,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
const resolveConstantOperand = () => {
|
||||
const operand = tokens[i];
|
||||
|
||||
if (!operand.inQuotes
|
||||
&& (operand.token.startsWith('#') || operand.token.startsWith('~') || operand.token === 'note')) {
|
||||
if (!operand.inQuotes && (operand.token.startsWith("#") || operand.token.startsWith("~") || operand.token === "note")) {
|
||||
searchContext.addError(`Error near token "${operand.token}" in ${context(i)}, it's possible to compare with constant only.`);
|
||||
return null;
|
||||
}
|
||||
@@ -99,12 +83,11 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
let delta = 0;
|
||||
|
||||
if (i + 2 < tokens.length) {
|
||||
if (tokens[i + 1].token === '+') {
|
||||
if (tokens[i + 1].token === "+") {
|
||||
i += 2;
|
||||
|
||||
delta += parseInt(tokens[i].token);
|
||||
}
|
||||
else if (tokens[i + 1].token === '-') {
|
||||
} else if (tokens[i + 1].token === "-") {
|
||||
i += 2;
|
||||
|
||||
delta -= parseInt(tokens[i].token);
|
||||
@@ -113,39 +96,35 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
|
||||
let format, date;
|
||||
|
||||
if (operand.token === 'now') {
|
||||
date = dayjs().add(delta, 'second');
|
||||
if (operand.token === "now") {
|
||||
date = dayjs().add(delta, "second");
|
||||
format = "YYYY-MM-DD HH:mm:ss";
|
||||
}
|
||||
else if (operand.token === 'today') {
|
||||
date = dayjs().add(delta, 'day');
|
||||
} else if (operand.token === "today") {
|
||||
date = dayjs().add(delta, "day");
|
||||
format = "YYYY-MM-DD";
|
||||
}
|
||||
else if (operand.token === 'month') {
|
||||
date = dayjs().add(delta, 'month');
|
||||
} else if (operand.token === "month") {
|
||||
date = dayjs().add(delta, "month");
|
||||
format = "YYYY-MM";
|
||||
}
|
||||
else if (operand.token === 'year') {
|
||||
date = dayjs().add(delta, 'year');
|
||||
} else if (operand.token === "year") {
|
||||
date = dayjs().add(delta, "year");
|
||||
format = "YYYY";
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(`Unrecognized keyword: ${operand.token}`);
|
||||
}
|
||||
|
||||
return date.format(format);
|
||||
}
|
||||
};
|
||||
|
||||
const parseNoteProperty: () => Expression | undefined | null = () => {
|
||||
if (tokens[i].token !== '.') {
|
||||
if (tokens[i].token !== ".") {
|
||||
searchContext.addError('Expected "." to separate field path');
|
||||
return;
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
if (['content', 'rawcontent'].includes(tokens[i].token)) {
|
||||
const raw = tokens[i].token === 'rawcontent';
|
||||
if (["content", "rawcontent"].includes(tokens[i].token)) {
|
||||
const raw = tokens[i].token === "rawcontent";
|
||||
|
||||
i += 1;
|
||||
|
||||
@@ -158,35 +137,41 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
|
||||
i++;
|
||||
|
||||
return new NoteContentFulltextExp(operator.token, {tokens: [tokens[i].token], raw});
|
||||
return new NoteContentFulltextExp(operator.token, { tokens: [tokens[i].token], raw });
|
||||
}
|
||||
|
||||
if (tokens[i].token === 'parents') {
|
||||
if (tokens[i].token === "parents") {
|
||||
i += 1;
|
||||
|
||||
const expression = parseNoteProperty();
|
||||
if (!expression) { return; }
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
return new ChildOfExp(expression);
|
||||
}
|
||||
|
||||
if (tokens[i].token === 'children') {
|
||||
if (tokens[i].token === "children") {
|
||||
i += 1;
|
||||
|
||||
const expression = parseNoteProperty();
|
||||
if (!expression) { return; }
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
return new ParentOfExp(expression);
|
||||
}
|
||||
|
||||
if (tokens[i].token === 'ancestors') {
|
||||
if (tokens[i].token === "ancestors") {
|
||||
i += 1;
|
||||
|
||||
const expression = parseNoteProperty();
|
||||
if (!expression) { return; }
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
return new DescendantOfExp(expression);
|
||||
}
|
||||
|
||||
if (tokens[i].token === 'labels') {
|
||||
if (tokens[i + 1].token !== '.') {
|
||||
if (tokens[i].token === "labels") {
|
||||
if (tokens[i + 1].token !== ".") {
|
||||
searchContext.addError(`Expected "." to separate field path, got "${tokens[i + 1].token}" in ${context(i)}`);
|
||||
return;
|
||||
}
|
||||
@@ -196,8 +181,8 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
return parseLabel(tokens[i].token);
|
||||
}
|
||||
|
||||
if (tokens[i].token === 'relations') {
|
||||
if (tokens[i + 1].token !== '.') {
|
||||
if (tokens[i].token === "relations") {
|
||||
if (tokens[i + 1].token !== ".") {
|
||||
searchContext.addError(`Expected "." to separate field path, got "${tokens[i + 1].token}" in ${context(i)}`);
|
||||
return;
|
||||
}
|
||||
@@ -207,18 +192,15 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
return parseRelation(tokens[i].token);
|
||||
}
|
||||
|
||||
if (tokens[i].token === 'text') {
|
||||
if (tokens[i + 1].token !== '*=*') {
|
||||
if (tokens[i].token === "text") {
|
||||
if (tokens[i + 1].token !== "*=*") {
|
||||
searchContext.addError(`Virtual attribute "note.text" supports only *=* operator, instead given "${tokens[i + 1].token}" in ${context(i)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
i += 2;
|
||||
|
||||
return new OrExp([
|
||||
new PropertyComparisonExp(searchContext, 'title', '*=*', tokens[i].token),
|
||||
new NoteContentFulltextExp('*=*', {tokens: [tokens[i].token]})
|
||||
]);
|
||||
return new OrExp([new PropertyComparisonExp(searchContext, "title", "*=*", tokens[i].token), new NoteContentFulltextExp("*=*", { tokens: [tokens[i].token] })]);
|
||||
}
|
||||
|
||||
if (PropertyComparisonExp.isProperty(tokens[i].token)) {
|
||||
@@ -237,14 +219,14 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
}
|
||||
|
||||
searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`);
|
||||
}
|
||||
};
|
||||
|
||||
function parseAttribute(name: string) {
|
||||
const isLabel = name.startsWith('#');
|
||||
const isLabel = name.startsWith("#");
|
||||
|
||||
name = name.substr(1);
|
||||
|
||||
const isNegated = name.startsWith('!');
|
||||
const isNegated = name.startsWith("!");
|
||||
|
||||
if (isNegated) {
|
||||
name = name.substr(1);
|
||||
@@ -271,8 +253,8 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
|
||||
searchContext.highlightedTokens.push(comparedValue);
|
||||
|
||||
if (searchContext.fuzzyAttributeSearch && operator === '=') {
|
||||
operator = '*=*';
|
||||
if (searchContext.fuzzyAttributeSearch && operator === "=") {
|
||||
operator = "*=*";
|
||||
}
|
||||
|
||||
const comparator = buildComparator(operator, comparedValue);
|
||||
@@ -280,41 +262,41 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
if (!comparator) {
|
||||
searchContext.addError(`Can't find operator '${operator}' in ${context(i - 1)}`);
|
||||
} else {
|
||||
return new LabelComparisonExp('label', labelName, comparator);
|
||||
return new LabelComparisonExp("label", labelName, comparator);
|
||||
}
|
||||
} else {
|
||||
return new AttributeExistsExp('label', labelName, searchContext.fuzzyAttributeSearch);
|
||||
return new AttributeExistsExp("label", labelName, searchContext.fuzzyAttributeSearch);
|
||||
}
|
||||
}
|
||||
|
||||
function parseRelation(relationName: string) {
|
||||
searchContext.highlightedTokens.push(relationName);
|
||||
|
||||
if (i < tokens.length - 2 && tokens[i + 1].token === '.') {
|
||||
if (i < tokens.length - 2 && tokens[i + 1].token === ".") {
|
||||
i += 1;
|
||||
|
||||
const expression = parseNoteProperty();
|
||||
if (!expression) { return; }
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
return new RelationWhereExp(relationName, expression);
|
||||
}
|
||||
else if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
|
||||
} else if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
|
||||
searchContext.addError(`Relation can be compared only with property, e.g. ~relation.title=hello in ${context(i)}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return new AttributeExistsExp('relation', relationName, searchContext.fuzzyAttributeSearch);
|
||||
} else {
|
||||
return new AttributeExistsExp("relation", relationName, searchContext.fuzzyAttributeSearch);
|
||||
}
|
||||
}
|
||||
|
||||
function parseOrderByAndLimit() {
|
||||
const orderDefinitions: {
|
||||
valueExtractor: ValueExtractor,
|
||||
direction: string
|
||||
valueExtractor: ValueExtractor;
|
||||
direction: string;
|
||||
}[] = [];
|
||||
let limit;
|
||||
|
||||
if (tokens[i].token === 'orderby') {
|
||||
if (tokens[i].token === "orderby") {
|
||||
do {
|
||||
const propertyPath = [];
|
||||
let direction = "asc";
|
||||
@@ -325,7 +307,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
propertyPath.push(tokens[i].token);
|
||||
|
||||
i++;
|
||||
} while (i < tokens.length && tokens[i].token === '.');
|
||||
} while (i < tokens.length && tokens[i].token === ".");
|
||||
|
||||
if (i < tokens.length && ["asc", "desc"].includes(tokens[i].token)) {
|
||||
direction = tokens[i].token;
|
||||
@@ -343,10 +325,10 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
valueExtractor,
|
||||
direction
|
||||
});
|
||||
} while (i < tokens.length && tokens[i].token === ',');
|
||||
} while (i < tokens.length && tokens[i].token === ",");
|
||||
}
|
||||
|
||||
if (i < tokens.length && tokens[i].token === 'limit') {
|
||||
if (i < tokens.length && tokens[i].token === "limit") {
|
||||
limit = parseInt(tokens[i + 1].token);
|
||||
}
|
||||
|
||||
@@ -354,13 +336,11 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
}
|
||||
|
||||
function getAggregateExpression() {
|
||||
if (op === null || op === 'and') {
|
||||
if (op === null || op === "and") {
|
||||
return AndExp.of(expressions);
|
||||
}
|
||||
else if (op === 'or') {
|
||||
} else if (op === "or") {
|
||||
return OrExp.of(expressions);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(`Unrecognized op=${op}`);
|
||||
}
|
||||
}
|
||||
@@ -376,19 +356,18 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
|
||||
const token = tokens[i].token;
|
||||
|
||||
if (token === '#' || token === '~') {
|
||||
if (token === "#" || token === "~") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.startsWith('#') || token.startsWith('~')) {
|
||||
if (token.startsWith("#") || token.startsWith("~")) {
|
||||
const attribute = parseAttribute(token);
|
||||
if (attribute) {
|
||||
expressions.push(attribute);
|
||||
}
|
||||
}
|
||||
else if (['orderby', 'limit'].includes(token)) {
|
||||
} else if (["orderby", "limit"].includes(token)) {
|
||||
if (level !== 0) {
|
||||
searchContext.addError('orderBy can appear only on the top expression level');
|
||||
searchContext.addError("orderBy can appear only on the top expression level");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -400,8 +379,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
|
||||
exp.subExpression = getAggregateExpression();
|
||||
return exp;
|
||||
}
|
||||
else if (token === 'not') {
|
||||
} else if (token === "not") {
|
||||
i += 1;
|
||||
|
||||
if (!Array.isArray(tokens[i])) {
|
||||
@@ -411,53 +389,46 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
||||
|
||||
const tokenArray = tokens[i] as unknown as TokenData[];
|
||||
const expression = getExpression(tokenArray, searchContext, level++);
|
||||
if (!expression) { return; }
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
expressions.push(new NotExp(expression));
|
||||
}
|
||||
else if (token === 'note') {
|
||||
} else if (token === "note") {
|
||||
i++;
|
||||
|
||||
const expression = parseNoteProperty();
|
||||
if (!expression) { return; }
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
expressions.push(expression);
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (['and', 'or'].includes(token)) {
|
||||
} else if (["and", "or"].includes(token)) {
|
||||
if (!op) {
|
||||
op = token;
|
||||
} else if (op !== token) {
|
||||
searchContext.addError("Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.");
|
||||
}
|
||||
else if (op !== token) {
|
||||
searchContext.addError('Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.');
|
||||
}
|
||||
}
|
||||
else if (isOperator({token: token})) {
|
||||
} else if (isOperator({ token: token })) {
|
||||
searchContext.addError(`Misplaced or incomplete expression "${token}"`);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
searchContext.addError(`Unrecognized expression "${token}"`);
|
||||
}
|
||||
|
||||
if (!op && expressions.length > 1) {
|
||||
op = 'and';
|
||||
op = "and";
|
||||
}
|
||||
}
|
||||
|
||||
return getAggregateExpression();
|
||||
}
|
||||
|
||||
function parse({fulltextTokens, expressionTokens, searchContext}: {
|
||||
fulltextTokens: TokenData[],
|
||||
expressionTokens: TokenStructure,
|
||||
searchContext: SearchContext,
|
||||
originalQuery: string
|
||||
}) {
|
||||
function parse({ fulltextTokens, expressionTokens, searchContext }: { fulltextTokens: TokenData[]; expressionTokens: TokenStructure; searchContext: SearchContext; originalQuery: string }) {
|
||||
let expression: Expression | undefined | null;
|
||||
|
||||
try {
|
||||
expression = getExpression(expressionTokens as TokenData[], searchContext);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
searchContext.addError(e.message);
|
||||
|
||||
expression = new TrueExp();
|
||||
@@ -470,13 +441,18 @@ function parse({fulltextTokens, expressionTokens, searchContext}: {
|
||||
expression
|
||||
]);
|
||||
|
||||
if (searchContext.orderBy && searchContext.orderBy !== 'relevancy') {
|
||||
if (searchContext.orderBy && searchContext.orderBy !== "relevancy") {
|
||||
const filterExp = exp;
|
||||
|
||||
exp = new OrderByAndLimitExp([{
|
||||
valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]),
|
||||
direction: searchContext.orderDirection
|
||||
}], searchContext.limit || undefined);
|
||||
exp = new OrderByAndLimitExp(
|
||||
[
|
||||
{
|
||||
valueExtractor: new ValueExtractor(searchContext, ["note", searchContext.orderBy]),
|
||||
direction: searchContext.orderDirection
|
||||
}
|
||||
],
|
||||
searchContext.limit || undefined
|
||||
);
|
||||
|
||||
(exp as any).subExpression = filterExp;
|
||||
}
|
||||
@@ -484,8 +460,8 @@ function parse({fulltextTokens, expressionTokens, searchContext}: {
|
||||
return exp;
|
||||
}
|
||||
|
||||
function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}: SearchContext) {
|
||||
if (ancestorNoteId && ancestorNoteId !== 'root') {
|
||||
function getAncestorExp({ ancestorNoteId, ancestorDepth, includeHiddenNotes }: SearchContext) {
|
||||
if (ancestorNoteId && ancestorNoteId !== "root") {
|
||||
return new AncestorExp(ancestorNoteId, ancestorDepth);
|
||||
} else if (!includeHiddenNotes) {
|
||||
return new NotExp(new IsHiddenExp());
|
||||
|
||||
@@ -34,28 +34,27 @@ function searchFromNote(note: BNote): SearchNoteResult {
|
||||
let searchResultNoteIds;
|
||||
let highlightedTokens: string[];
|
||||
|
||||
const searchScript = note.getRelationValue('searchScript');
|
||||
const searchString = note.getLabelValue('searchString') || "";
|
||||
const searchScript = note.getRelationValue("searchScript");
|
||||
const searchString = note.getLabelValue("searchString") || "";
|
||||
let error = null;
|
||||
|
||||
if (searchScript) {
|
||||
searchResultNoteIds = searchFromRelation(note, 'searchScript');
|
||||
searchResultNoteIds = searchFromRelation(note, "searchScript");
|
||||
highlightedTokens = [];
|
||||
} else {
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: note.hasLabel('fastSearch'),
|
||||
ancestorNoteId: note.getRelationValue('ancestor') || undefined,
|
||||
ancestorDepth: note.getLabelValue('ancestorDepth') || undefined,
|
||||
includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
|
||||
orderBy: note.getLabelValue('orderBy') || undefined,
|
||||
orderDirection: note.getLabelValue('orderDirection') || undefined,
|
||||
limit: parseInt(note.getLabelValue('limit') || "0", 10),
|
||||
debug: note.hasLabel('debug'),
|
||||
fastSearch: note.hasLabel("fastSearch"),
|
||||
ancestorNoteId: note.getRelationValue("ancestor") || undefined,
|
||||
ancestorDepth: note.getLabelValue("ancestorDepth") || undefined,
|
||||
includeArchivedNotes: note.hasLabel("includeArchivedNotes"),
|
||||
orderBy: note.getLabelValue("orderBy") || undefined,
|
||||
orderDirection: note.getLabelValue("orderDirection") || undefined,
|
||||
limit: parseInt(note.getLabelValue("limit") || "0", 10),
|
||||
debug: note.hasLabel("debug"),
|
||||
fuzzyAttributeSearch: false
|
||||
});
|
||||
|
||||
searchResultNoteIds = findResultsWithQuery(searchString, searchContext)
|
||||
.map(sr => sr.noteId);
|
||||
searchResultNoteIds = findResultsWithQuery(searchString, searchContext).map((sr) => sr.noteId);
|
||||
|
||||
highlightedTokens = searchContext.highlightedTokens;
|
||||
error = searchContext.getError();
|
||||
@@ -64,7 +63,7 @@ function searchFromNote(note: BNote): SearchNoteResult {
|
||||
// we won't return search note's own noteId
|
||||
// also don't allow root since that would force infinite cycle
|
||||
return {
|
||||
searchResultNoteIds: searchResultNoteIds.filter(resultNoteId => !['root', note.noteId].includes(resultNoteId)),
|
||||
searchResultNoteIds: searchResultNoteIds.filter((resultNoteId) => !["root", note.noteId].includes(resultNoteId)),
|
||||
highlightedTokens,
|
||||
error: error
|
||||
};
|
||||
@@ -79,7 +78,7 @@ function searchFromRelation(note: BNote, relationName: string) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!scriptNote.isJavaScript() || scriptNote.getScriptEnv() !== 'backend') {
|
||||
if (!scriptNote.isJavaScript() || scriptNote.getScriptEnv() !== "backend") {
|
||||
log.info(`Note ${scriptNote.noteId} is not executable.`);
|
||||
|
||||
return [];
|
||||
@@ -91,7 +90,7 @@ function searchFromRelation(note: BNote, relationName: string) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = scriptService.executeNote(scriptNote, {originEntity: note});
|
||||
const result = scriptService.executeNote(scriptNote, { originEntity: note });
|
||||
|
||||
if (!Array.isArray(result)) {
|
||||
log.info(`Result from ${scriptNote.noteId} is not an array.`);
|
||||
@@ -104,7 +103,7 @@ function searchFromRelation(note: BNote, relationName: string) {
|
||||
}
|
||||
|
||||
// we expect either array of noteIds (strings) or notes, in that case we extract noteIds ourselves
|
||||
return typeof result[0] === 'string' ? result : result.map(item => item.noteId);
|
||||
return typeof result[0] === "string" ? result : result.map((item) => item.noteId);
|
||||
}
|
||||
|
||||
function loadNeededInfoFromDatabase() {
|
||||
@@ -131,7 +130,7 @@ function loadNeededInfoFromDatabase() {
|
||||
JOIN blobs USING(blobId)
|
||||
WHERE notes.isDeleted = 0`);
|
||||
|
||||
for (const {noteId, blobId, length} of noteContentLengths) {
|
||||
for (const { noteId, blobId, length } of noteContentLengths) {
|
||||
if (!(noteId in becca.notes)) {
|
||||
log.error(`Note '${noteId}' not found in becca.`);
|
||||
continue;
|
||||
@@ -159,7 +158,7 @@ function loadNeededInfoFromDatabase() {
|
||||
WHERE attachments.isDeleted = 0
|
||||
AND notes.isDeleted = 0`);
|
||||
|
||||
for (const {noteId, blobId, length} of attachmentContentLengths) {
|
||||
for (const { noteId, blobId, length } of attachmentContentLengths) {
|
||||
if (!(noteId in becca.notes)) {
|
||||
log.error(`Note '${noteId}' not found in becca.`);
|
||||
continue;
|
||||
@@ -205,7 +204,7 @@ function loadNeededInfoFromDatabase() {
|
||||
JOIN blobs ON attachments.blobId = blobs.blobId
|
||||
WHERE notes.isDeleted = 0`);
|
||||
|
||||
for (const {noteId, blobId, length, isNoteRevision} of revisionContentLengths) {
|
||||
for (const { noteId, blobId, length, isNoteRevision } of revisionContentLengths) {
|
||||
if (!(noteId in becca.notes)) {
|
||||
log.error(`Note '${noteId}' not found in becca.`);
|
||||
continue;
|
||||
@@ -218,7 +217,7 @@ function loadNeededInfoFromDatabase() {
|
||||
|
||||
noteBlobs[noteId][blobId] = length;
|
||||
|
||||
if (isNoteRevision) {
|
||||
if (isNoteRevision) {
|
||||
const noteRevision = becca.notes[noteId];
|
||||
if (noteRevision && noteRevision.revisionCount) {
|
||||
noteRevision.revisionCount++;
|
||||
@@ -245,16 +244,15 @@ function findResultsWithExpression(expression: Expression, searchContext: Search
|
||||
|
||||
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
|
||||
|
||||
const searchResults = noteSet.notes
|
||||
.map(note => {
|
||||
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
|
||||
const searchResults = noteSet.notes.map((note) => {
|
||||
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
|
||||
|
||||
if (!notePathArray) {
|
||||
throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
|
||||
}
|
||||
if (!notePathArray) {
|
||||
throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
|
||||
}
|
||||
|
||||
return new SearchResult(notePathArray);
|
||||
});
|
||||
return new SearchResult(notePathArray);
|
||||
});
|
||||
|
||||
for (const res of searchResults) {
|
||||
res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens);
|
||||
@@ -282,15 +280,14 @@ function findResultsWithExpression(expression: Expression, searchContext: Search
|
||||
}
|
||||
|
||||
function parseQueryToExpression(query: string, searchContext: SearchContext) {
|
||||
const {fulltextQuery, fulltextTokens, expressionTokens} = lex(query);
|
||||
const { fulltextQuery, fulltextTokens, expressionTokens } = lex(query);
|
||||
searchContext.fulltextQuery = fulltextQuery;
|
||||
|
||||
let structuredExpressionTokens: TokenStructure;
|
||||
|
||||
try {
|
||||
structuredExpressionTokens = handleParens(expressionTokens);
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
structuredExpressionTokens = [];
|
||||
searchContext.addError(e.message);
|
||||
}
|
||||
@@ -318,7 +315,7 @@ function parseQueryToExpression(query: string, searchContext: SearchContext) {
|
||||
function searchNotes(query: string, params: SearchParams = {}): BNote[] {
|
||||
const searchResults = findResultsWithQuery(query, new SearchContext(params));
|
||||
|
||||
return searchResults.map(sr => becca.notes[sr.noteId]);
|
||||
return searchResults.map((sr) => becca.notes[sr.noteId]);
|
||||
}
|
||||
|
||||
function findResultsWithQuery(query: string, searchContext: SearchContext): SearchResult[] {
|
||||
@@ -347,9 +344,7 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
|
||||
includeHiddenNotes: true,
|
||||
fuzzyAttributeSearch: true,
|
||||
ignoreInternalAttributes: true,
|
||||
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree()
|
||||
? 'root'
|
||||
: hoistedNoteService.getHoistedNoteId()
|
||||
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree() ? "root" : hoistedNoteService.getHoistedNoteId()
|
||||
});
|
||||
|
||||
const allSearchResults = findResultsWithQuery(query, searchContext);
|
||||
@@ -358,7 +353,7 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
|
||||
|
||||
highlightSearchResults(trimmed, searchContext.highlightedTokens, searchContext.ignoreInternalAttributes);
|
||||
|
||||
return trimmed.map(result => {
|
||||
return trimmed.map((result) => {
|
||||
return {
|
||||
notePath: result.notePath,
|
||||
noteTitle: beccaService.getNoteTitle(result.noteId),
|
||||
@@ -378,23 +373,21 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
||||
// which would make the resulting HTML string invalid.
|
||||
// { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character)
|
||||
// < and > are used for marking <small> and </small>
|
||||
highlightedTokens = highlightedTokens
|
||||
.map(token => token.replace('/[<\{\}]/g', ''))
|
||||
.filter(token => !!token?.trim());
|
||||
highlightedTokens = highlightedTokens.map((token) => token.replace("/[<\{\}]/g", "")).filter((token) => !!token?.trim());
|
||||
|
||||
// sort by the longest, so we first highlight the longest matches
|
||||
highlightedTokens.sort((a, b) => a.length > b.length ? -1 : 1);
|
||||
highlightedTokens.sort((a, b) => (a.length > b.length ? -1 : 1));
|
||||
|
||||
for (const result of searchResults) {
|
||||
const note = becca.notes[result.noteId];
|
||||
|
||||
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, '');
|
||||
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, "");
|
||||
|
||||
if (highlightedTokens.find(token => note.type.includes(token))) {
|
||||
if (highlightedTokens.find((token) => note.type.includes(token))) {
|
||||
result.highlightedNotePathTitle += ` "type: ${note.type}'`;
|
||||
}
|
||||
|
||||
if (highlightedTokens.find(token => note.mime.includes(token))) {
|
||||
if (highlightedTokens.find((token) => note.mime.includes(token))) {
|
||||
result.highlightedNotePathTitle += ` "mime: ${note.mime}'`;
|
||||
}
|
||||
|
||||
@@ -403,9 +396,7 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
||||
continue;
|
||||
}
|
||||
|
||||
if (highlightedTokens.find(token => normalize(attr.name).includes(token)
|
||||
|| normalize(attr.value).includes(token))) {
|
||||
|
||||
if (highlightedTokens.find((token) => normalize(attr.name).includes(token) || normalize(attr.value).includes(token))) {
|
||||
result.highlightedNotePathTitle += ` "${formatAttribute(attr)}'`;
|
||||
}
|
||||
}
|
||||
@@ -427,7 +418,9 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
||||
let match;
|
||||
|
||||
// Find all matches
|
||||
if (!result.highlightedNotePathTitle) { continue; }
|
||||
if (!result.highlightedNotePathTitle) {
|
||||
continue;
|
||||
}
|
||||
while ((match = tokenRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) {
|
||||
result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}");
|
||||
|
||||
@@ -438,20 +431,17 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
||||
}
|
||||
|
||||
for (const result of searchResults) {
|
||||
if (!result.highlightedNotePathTitle) { continue; }
|
||||
result.highlightedNotePathTitle = result.highlightedNotePathTitle
|
||||
.replace(/"/g, "<small>")
|
||||
.replace(/'/g, "</small>")
|
||||
.replace(/{/g, "<b>")
|
||||
.replace(/}/g, "</b>");
|
||||
if (!result.highlightedNotePathTitle) {
|
||||
continue;
|
||||
}
|
||||
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/"/g, "<small>").replace(/'/g, "</small>").replace(/{/g, "<b>").replace(/}/g, "</b>");
|
||||
}
|
||||
}
|
||||
|
||||
function formatAttribute(attr: BAttribute) {
|
||||
if (attr.type === 'relation') {
|
||||
if (attr.type === "relation") {
|
||||
return `~${escapeHtml(attr.name)}=…`;
|
||||
}
|
||||
else if (attr.type === 'label') {
|
||||
} else if (attr.type === "label") {
|
||||
let label = `#${escapeHtml(attr.name)}`;
|
||||
|
||||
if (attr.value) {
|
||||
|
||||
@@ -21,4 +21,4 @@ export interface SearchParams {
|
||||
limit?: number | null;
|
||||
debug?: boolean;
|
||||
fuzzyAttributeSearch?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,31 +7,31 @@ import BNote from "../../becca/entities/bnote.js";
|
||||
* we need a case-sensitive form, so we have this translation object.
|
||||
*/
|
||||
const PROP_MAPPING: Record<string, string> = {
|
||||
"noteid": "noteId",
|
||||
"title": "title",
|
||||
"type": "type",
|
||||
"mime": "mime",
|
||||
"isprotected": "isProtected",
|
||||
"isarchived": "isArchived",
|
||||
"datecreated": "dateCreated",
|
||||
"datemodified": "dateModified",
|
||||
"utcdatecreated": "utcDateCreated",
|
||||
"utcdatemodified": "utcDateModified",
|
||||
"parentcount": "parentCount",
|
||||
"childrencount": "childrenCount",
|
||||
"attributecount": "attributeCount",
|
||||
"labelcount": "labelCount",
|
||||
"ownedlabelcount": "ownedLabelCount",
|
||||
"relationcount": "relationCount",
|
||||
"ownedrelationcount": "ownedRelationCount",
|
||||
"relationcountincludinglinks": "relationCountIncludingLinks",
|
||||
"ownedrelationcountincludinglinks": "ownedRelationCountIncludingLinks",
|
||||
"targetrelationcount": "targetRelationCount",
|
||||
"targetrelationcountincludinglinks": "targetRelationCountIncludingLinks",
|
||||
"contentsize": "contentSize",
|
||||
"contentandattachmentssize": "contentAndAttachmentsSize",
|
||||
"contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize",
|
||||
"revisioncount": "revisionCount"
|
||||
noteid: "noteId",
|
||||
title: "title",
|
||||
type: "type",
|
||||
mime: "mime",
|
||||
isprotected: "isProtected",
|
||||
isarchived: "isArchived",
|
||||
datecreated: "dateCreated",
|
||||
datemodified: "dateModified",
|
||||
utcdatecreated: "utcDateCreated",
|
||||
utcdatemodified: "utcDateModified",
|
||||
parentcount: "parentCount",
|
||||
childrencount: "childrenCount",
|
||||
attributecount: "attributeCount",
|
||||
labelcount: "labelCount",
|
||||
ownedlabelcount: "ownedLabelCount",
|
||||
relationcount: "relationCount",
|
||||
ownedrelationcount: "ownedRelationCount",
|
||||
relationcountincludinglinks: "relationCountIncludingLinks",
|
||||
ownedrelationcountincludinglinks: "ownedRelationCountIncludingLinks",
|
||||
targetrelationcount: "targetRelationCount",
|
||||
targetrelationcountincludinglinks: "targetRelationCountIncludingLinks",
|
||||
contentsize: "contentSize",
|
||||
contentandattachmentssize: "contentAndAttachmentsSize",
|
||||
contentandattachmentsandrevisionssize: "contentAndAttachmentsAndRevisionsSize",
|
||||
revisioncount: "revisionCount"
|
||||
};
|
||||
|
||||
interface SearchContext {
|
||||
@@ -42,48 +42,44 @@ class ValueExtractor {
|
||||
private propertyPath: string[];
|
||||
|
||||
constructor(searchContext: SearchContext, propertyPath: string[]) {
|
||||
this.propertyPath = propertyPath.map(pathEl => pathEl.toLowerCase());
|
||||
this.propertyPath = propertyPath.map((pathEl) => pathEl.toLowerCase());
|
||||
|
||||
if (this.propertyPath[0].startsWith('#')) {
|
||||
this.propertyPath = ['note', 'labels', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
|
||||
}
|
||||
else if (this.propertyPath[0].startsWith('~')) {
|
||||
this.propertyPath = ['note', 'relations', this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
|
||||
if (this.propertyPath[0].startsWith("#")) {
|
||||
this.propertyPath = ["note", "labels", this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
|
||||
} else if (this.propertyPath[0].startsWith("~")) {
|
||||
this.propertyPath = ["note", "relations", this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
|
||||
}
|
||||
|
||||
if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyPath[this.propertyPath.length - 1])) {
|
||||
if (["contentsize", "contentandattachmentssize", "contentandattachmentsandrevisionssize", "revisioncount"].includes(this.propertyPath[this.propertyPath.length - 1])) {
|
||||
searchContext.dbLoadNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (this.propertyPath[0] !== 'note') {
|
||||
if (this.propertyPath[0] !== "note") {
|
||||
return `property specifier must start with 'note', but starts with '${this.propertyPath[0]}'`;
|
||||
}
|
||||
|
||||
for (let i = 1; i < this.propertyPath.length; i++) {
|
||||
const pathEl = this.propertyPath[i];
|
||||
|
||||
if (pathEl === 'labels') {
|
||||
if (pathEl === "labels") {
|
||||
if (i !== this.propertyPath.length - 2) {
|
||||
return `label is a terminal property specifier and must be at the end`;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
else if (pathEl === 'relations') {
|
||||
} else if (pathEl === "relations") {
|
||||
if (i >= this.propertyPath.length - 2) {
|
||||
return `relation name or property name is missing`;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
else if (pathEl in PROP_MAPPING || pathEl === 'random') {
|
||||
} else if (pathEl in PROP_MAPPING || pathEl === "random") {
|
||||
if (i !== this.propertyPath.length - 1) {
|
||||
return `${pathEl} is a terminal property specifier and must be at the end`;
|
||||
}
|
||||
}
|
||||
else if (!["parents", "children"].includes(pathEl)) {
|
||||
} else if (!["parents", "children"].includes(pathEl)) {
|
||||
return `Unrecognized property specifier ${pathEl}`;
|
||||
}
|
||||
}
|
||||
@@ -101,33 +97,28 @@ class ValueExtractor {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
if (cur() === 'labels') {
|
||||
if (cur() === "labels") {
|
||||
i++;
|
||||
|
||||
const attr = cursor.getAttributeCaseInsensitive('label', cur());
|
||||
const attr = cursor.getAttributeCaseInsensitive("label", cur());
|
||||
|
||||
return attr ? attr.value : null;
|
||||
}
|
||||
|
||||
if (cur() === 'relations') {
|
||||
if (cur() === "relations") {
|
||||
i++;
|
||||
|
||||
const attr = cursor.getAttributeCaseInsensitive('relation', cur());
|
||||
const attr = cursor.getAttributeCaseInsensitive("relation", cur());
|
||||
cursor = attr?.targetNote || null;
|
||||
}
|
||||
else if (cur() === 'parents') {
|
||||
} else if (cur() === "parents") {
|
||||
cursor = cursor.parents[0];
|
||||
}
|
||||
else if (cur() === 'children') {
|
||||
} else if (cur() === "children") {
|
||||
cursor = cursor.children[0];
|
||||
}
|
||||
else if (cur() === 'random') {
|
||||
} else if (cur() === "random") {
|
||||
return Math.random().toString(); // string is expected for comparison
|
||||
}
|
||||
else if (cur() in PROP_MAPPING) {
|
||||
} else if (cur() in PROP_MAPPING) {
|
||||
return (cursor as any)[PROP_MAPPING[cur()]];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// FIXME
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import fs from "fs";
|
||||
import dataDir from "./data_dir.js";
|
||||
import log from "./log.js";
|
||||
import { randomSecureToken } from "./utils.js"
|
||||
import { randomSecureToken } from "./utils.js";
|
||||
|
||||
const sessionSecretPath = `${dataDir.TRILIUM_DATA_DIR}/session_secret.txt`;
|
||||
|
||||
@@ -17,8 +17,7 @@ if (!fs.existsSync(sessionSecretPath)) {
|
||||
log.info("Generated session secret");
|
||||
|
||||
fs.writeFileSync(sessionSecretPath, sessionSecret, ENCODING);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
sessionSecret = fs.readFileSync(sessionSecretPath, ENCODING);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,15 @@ import request from "./request.js";
|
||||
import appInfo from "./app_info.js";
|
||||
import { timeLimit } from "./utils.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import { SetupStatusResponse, SetupSyncSeedResponse } from './api-interface.js';
|
||||
import { SetupStatusResponse, SetupSyncSeedResponse } from "./api-interface.js";
|
||||
|
||||
async function hasSyncServerSchemaAndSeed() {
|
||||
const response = await requestToSyncServer<SetupStatusResponse>('GET', '/api/setup/status');
|
||||
const response = await requestToSyncServer<SetupStatusResponse>("GET", "/api/setup/status");
|
||||
|
||||
if (response.syncVersion !== appInfo.syncVersion) {
|
||||
throw new Error(`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`);
|
||||
throw new Error(
|
||||
`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`
|
||||
);
|
||||
}
|
||||
|
||||
return response.schemaExists;
|
||||
@@ -23,7 +25,7 @@ function triggerSync() {
|
||||
log.info("Triggering sync.");
|
||||
|
||||
// it's ok to not wait for it here
|
||||
syncService.sync().then(res => {
|
||||
syncService.sync().then((res) => {
|
||||
if (res.success) {
|
||||
sqlInit.setDbAsInitialized();
|
||||
}
|
||||
@@ -33,34 +35,37 @@ function triggerSync() {
|
||||
async function sendSeedToSyncServer() {
|
||||
log.info("Initiating sync to server");
|
||||
|
||||
await requestToSyncServer<void>('POST', '/api/setup/sync-seed', {
|
||||
await requestToSyncServer<void>("POST", "/api/setup/sync-seed", {
|
||||
options: getSyncSeedOptions(),
|
||||
syncVersion: appInfo.syncVersion
|
||||
});
|
||||
|
||||
// this is a completely new sync, need to reset counters. If this was not a new sync,
|
||||
// the previous request would have failed.
|
||||
optionService.setOption('lastSyncedPush', 0);
|
||||
optionService.setOption('lastSyncedPull', 0);
|
||||
optionService.setOption("lastSyncedPush", 0);
|
||||
optionService.setOption("lastSyncedPull", 0);
|
||||
}
|
||||
|
||||
async function requestToSyncServer<T>(method: string, path: string, body?: string | {}): Promise<T> {
|
||||
const timeout = syncOptions.getSyncTimeout();
|
||||
|
||||
return await timeLimit(request.exec({
|
||||
method,
|
||||
url: syncOptions.getSyncServerHost() + path,
|
||||
body,
|
||||
proxy: syncOptions.getSyncProxy(),
|
||||
timeout: timeout
|
||||
}), timeout) as T;
|
||||
return (await timeLimit(
|
||||
request.exec({
|
||||
method,
|
||||
url: syncOptions.getSyncServerHost() + path,
|
||||
body,
|
||||
proxy: syncOptions.getSyncProxy(),
|
||||
timeout: timeout
|
||||
}),
|
||||
timeout
|
||||
)) as T;
|
||||
}
|
||||
|
||||
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
|
||||
if (sqlInit.isDbInitialized()) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: 'DB is already initialized.'
|
||||
result: "failure",
|
||||
error: "DB is already initialized."
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,7 +74,7 @@ async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string
|
||||
|
||||
// the response is expected to contain documentId and documentSecret options
|
||||
const resp = await request.exec<SetupSyncSeedResponse>({
|
||||
method: 'get',
|
||||
method: "get",
|
||||
url: `${syncServerHost}/api/setup/sync-seed`,
|
||||
auth: { password },
|
||||
proxy: syncProxy,
|
||||
@@ -82,32 +87,28 @@ async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string
|
||||
log.error(message);
|
||||
|
||||
return {
|
||||
result: 'failure',
|
||||
result: "failure",
|
||||
error: message
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await sqlInit.createDatabaseForSync(resp.options, syncServerHost, syncProxy);
|
||||
|
||||
triggerSync();
|
||||
|
||||
return { result: 'success' };
|
||||
}
|
||||
catch (e: any) {
|
||||
return { result: "success" };
|
||||
} catch (e: any) {
|
||||
log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
|
||||
|
||||
return {
|
||||
result: 'failure',
|
||||
result: "failure",
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSyncSeedOptions() {
|
||||
return [
|
||||
becca.getOption('documentId'),
|
||||
becca.getOption('documentSecret')
|
||||
];
|
||||
return [becca.getOption("documentId"), becca.getOption("documentSecret")];
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
type Updater = () => void;
|
||||
|
||||
class SpacedUpdate {
|
||||
|
||||
private updater: Updater;
|
||||
private lastUpdated: number;
|
||||
private changed: boolean;
|
||||
@@ -29,8 +28,7 @@ class SpacedUpdate {
|
||||
|
||||
try {
|
||||
await this.updater();
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
this.changed = true;
|
||||
|
||||
throw e;
|
||||
@@ -55,8 +53,7 @@ class SpacedUpdate {
|
||||
this.updater();
|
||||
this.lastUpdated = Date.now();
|
||||
this.changed = false;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// update isn't triggered but changes are still pending, so we need to schedule another check
|
||||
this.scheduleUpdate();
|
||||
}
|
||||
@@ -67,8 +64,7 @@ class SpacedUpdate {
|
||||
|
||||
try {
|
||||
await callback();
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
this.changeForbidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,35 +20,33 @@ function getInboxNote(date: string) {
|
||||
let inbox;
|
||||
|
||||
if (!workspaceNote.isRoot()) {
|
||||
inbox = workspaceNote.searchNoteInSubtree('#workspaceInbox');
|
||||
inbox = workspaceNote.searchNoteInSubtree("#workspaceInbox");
|
||||
|
||||
if (!inbox) {
|
||||
inbox = workspaceNote.searchNoteInSubtree('#inbox');
|
||||
inbox = workspaceNote.searchNoteInSubtree("#inbox");
|
||||
}
|
||||
|
||||
if (!inbox) {
|
||||
inbox = workspaceNote;
|
||||
}
|
||||
}
|
||||
else {
|
||||
inbox = attributeService.getNoteWithLabel('inbox')
|
||||
|| dateNoteService.getDayNote(date);
|
||||
} else {
|
||||
inbox = attributeService.getNoteWithLabel("inbox") || dateNoteService.getDayNote(date);
|
||||
}
|
||||
|
||||
return inbox;
|
||||
}
|
||||
|
||||
function createSqlConsole() {
|
||||
const {note} = noteService.createNewNote({
|
||||
parentNoteId: getMonthlyParentNoteId('_sqlConsole', 'sqlConsole'),
|
||||
title: 'SQL Console - ' + dateUtils.localNowDate(),
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: getMonthlyParentNoteId("_sqlConsole", "sqlConsole"),
|
||||
title: "SQL Console - " + dateUtils.localNowDate(),
|
||||
content: "SELECT title, isDeleted, isProtected FROM notes WHERE noteId = ''\n\n\n\n",
|
||||
type: 'code',
|
||||
mime: 'text/x-sqlite;schema=trilium'
|
||||
type: "code",
|
||||
mime: "text/x-sqlite;schema=trilium"
|
||||
});
|
||||
|
||||
note.setLabel('iconClass', 'bx bx-data');
|
||||
note.setLabel('keepCurrentHoisting');
|
||||
note.setLabel("iconClass", "bx bx-data");
|
||||
note.setLabel("keepCurrentHoisting");
|
||||
|
||||
return note;
|
||||
}
|
||||
@@ -58,14 +56,12 @@ function saveSqlConsole(sqlConsoleNoteId: string) {
|
||||
if (!sqlConsoleNote) throw new Error(`Unable to find SQL console note ID: ${sqlConsoleNoteId}`);
|
||||
const today = dateUtils.localNowDate();
|
||||
|
||||
const sqlConsoleHome =
|
||||
attributeService.getNoteWithLabel('sqlConsoleHome')
|
||||
|| dateNoteService.getDayNote(today);
|
||||
const sqlConsoleHome = attributeService.getNoteWithLabel("sqlConsoleHome") || dateNoteService.getDayNote(today);
|
||||
|
||||
const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
|
||||
|
||||
for (const parentBranch of sqlConsoleNote.getParentBranches()) {
|
||||
if (parentBranch.parentNote?.hasAncestor('_hidden')) {
|
||||
if (parentBranch.parentNote?.hasAncestor("_hidden")) {
|
||||
parentBranch.markAsDeleted();
|
||||
}
|
||||
}
|
||||
@@ -74,19 +70,19 @@ function saveSqlConsole(sqlConsoleNoteId: string) {
|
||||
}
|
||||
|
||||
function createSearchNote(searchString: string, ancestorNoteId: string) {
|
||||
const {note} = noteService.createNewNote({
|
||||
parentNoteId: getMonthlyParentNoteId('_search', 'search'),
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: getMonthlyParentNoteId("_search", "search"),
|
||||
title: `${t("special_notes.search_prefix")} ${searchString}`,
|
||||
content: "",
|
||||
type: 'search',
|
||||
mime: 'application/json'
|
||||
type: "search",
|
||||
mime: "application/json"
|
||||
});
|
||||
|
||||
note.setLabel('searchString', searchString);
|
||||
note.setLabel('keepCurrentHoisting');
|
||||
note.setLabel("searchString", searchString);
|
||||
note.setLabel("keepCurrentHoisting");
|
||||
|
||||
if (ancestorNoteId) {
|
||||
note.setRelation('ancestor', ancestorNoteId);
|
||||
note.setRelation("ancestor", ancestorNoteId);
|
||||
}
|
||||
|
||||
return note;
|
||||
@@ -99,14 +95,11 @@ function getSearchHome() {
|
||||
}
|
||||
|
||||
if (!workspaceNote.isRoot()) {
|
||||
return workspaceNote.searchNoteInSubtree('#workspaceSearchHome')
|
||||
|| workspaceNote.searchNoteInSubtree('#searchHome')
|
||||
|| workspaceNote;
|
||||
return workspaceNote.searchNoteInSubtree("#workspaceSearchHome") || workspaceNote.searchNoteInSubtree("#searchHome") || workspaceNote;
|
||||
} else {
|
||||
const today = dateUtils.localNowDate();
|
||||
|
||||
return workspaceNote.searchNoteInSubtree('#searchHome')
|
||||
|| dateNoteService.getDayNote(today);
|
||||
return workspaceNote.searchNoteInSubtree("#searchHome") || dateNoteService.getDayNote(today);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +114,7 @@ function saveSearchNote(searchNoteId: string) {
|
||||
const result = searchNote.cloneTo(searchHome.noteId);
|
||||
|
||||
for (const parentBranch of searchNote.getParentBranches()) {
|
||||
if (parentBranch.parentNote?.hasAncestor('_hidden')) {
|
||||
if (parentBranch.parentNote?.hasAncestor("_hidden")) {
|
||||
parentBranch.markAsDeleted();
|
||||
}
|
||||
}
|
||||
@@ -133,17 +126,16 @@ function getMonthlyParentNoteId(rootNoteId: string, prefix: string) {
|
||||
const month = dateUtils.localNowDate().substring(0, 7);
|
||||
const labelName = `${prefix}MonthNote`;
|
||||
|
||||
let monthNote = searchService.findFirstNoteWithQuery(`#${labelName}="${month}"`,
|
||||
new SearchContext({ancestorNoteId: rootNoteId}));
|
||||
let monthNote = searchService.findFirstNoteWithQuery(`#${labelName}="${month}"`, new SearchContext({ ancestorNoteId: rootNoteId }));
|
||||
|
||||
if (!monthNote) {
|
||||
monthNote = noteService.createNewNote({
|
||||
parentNoteId: rootNoteId,
|
||||
title: month,
|
||||
content: '',
|
||||
content: "",
|
||||
isProtected: false,
|
||||
type: 'book'
|
||||
}).note
|
||||
type: "book"
|
||||
}).note;
|
||||
|
||||
monthNote.addLabel(labelName, month);
|
||||
}
|
||||
@@ -155,12 +147,12 @@ function createScriptLauncher(parentNoteId: string, forceNoteId?: string) {
|
||||
const note = noteService.createNewNote({
|
||||
noteId: forceNoteId,
|
||||
title: "Script Launcher",
|
||||
type: 'launcher',
|
||||
content: '',
|
||||
type: "launcher",
|
||||
content: "",
|
||||
parentNoteId: parentNoteId
|
||||
}).note;
|
||||
|
||||
note.addRelation('template', LBTPL_SCRIPT);
|
||||
note.addRelation("template", LBTPL_SCRIPT);
|
||||
return note;
|
||||
}
|
||||
|
||||
@@ -173,39 +165,39 @@ interface LauncherConfig {
|
||||
function createLauncher({ parentNoteId, launcherType, noteId }: LauncherConfig) {
|
||||
let note;
|
||||
|
||||
if (launcherType === 'note') {
|
||||
if (launcherType === "note") {
|
||||
note = noteService.createNewNote({
|
||||
noteId: noteId,
|
||||
title: "Note Launcher",
|
||||
type: 'launcher',
|
||||
content: '',
|
||||
type: "launcher",
|
||||
content: "",
|
||||
parentNoteId: parentNoteId
|
||||
}).note;
|
||||
|
||||
note.addRelation('template', LBTPL_NOTE_LAUNCHER);
|
||||
} else if (launcherType === 'script') {
|
||||
note.addRelation("template", LBTPL_NOTE_LAUNCHER);
|
||||
} else if (launcherType === "script") {
|
||||
note = createScriptLauncher(parentNoteId, noteId);
|
||||
} else if (launcherType === 'customWidget') {
|
||||
} else if (launcherType === "customWidget") {
|
||||
note = noteService.createNewNote({
|
||||
noteId: noteId,
|
||||
title: "Widget Launcher",
|
||||
type: 'launcher',
|
||||
content: '',
|
||||
type: "launcher",
|
||||
content: "",
|
||||
parentNoteId: parentNoteId
|
||||
}).note;
|
||||
|
||||
note.addRelation('template', LBTPL_CUSTOM_WIDGET);
|
||||
} else if (launcherType === 'spacer') {
|
||||
note.addRelation("template", LBTPL_CUSTOM_WIDGET);
|
||||
} else if (launcherType === "spacer") {
|
||||
note = noteService.createNewNote({
|
||||
noteId: noteId,
|
||||
branchId: noteId,
|
||||
title: "Spacer",
|
||||
type: 'launcher',
|
||||
content: '',
|
||||
type: "launcher",
|
||||
content: "",
|
||||
parentNoteId: parentNoteId
|
||||
}).note;
|
||||
|
||||
note.addRelation('template', LBTPL_SPACER);
|
||||
note.addRelation("template", LBTPL_SPACER);
|
||||
} else {
|
||||
throw new Error(`Unrecognized launcher type '${launcherType}'`);
|
||||
}
|
||||
@@ -221,7 +213,7 @@ function resetLauncher(noteId: string) {
|
||||
|
||||
if (note?.isLaunchBarConfig()) {
|
||||
if (note) {
|
||||
if (noteId === '_lbRoot') {
|
||||
if (noteId === "_lbRoot") {
|
||||
// deleting hoisted notes are not allowed, so we just reset the children
|
||||
for (const childNote of note.getChildNotes()) {
|
||||
childNote.deleteNote();
|
||||
@@ -247,42 +239,35 @@ function resetLauncher(noteId: string) {
|
||||
* Another use case was for script-packages (e.g. demo Task manager) which could this way register automatically/easily
|
||||
* into the launchbar - for this it's recommended to use backend API's createOrUpdateLauncher()
|
||||
*/
|
||||
function createOrUpdateScriptLauncherFromApi(opts: {
|
||||
id: string;
|
||||
title: string;
|
||||
action: string;
|
||||
icon?: string;
|
||||
shortcut?: string;
|
||||
}) {
|
||||
function createOrUpdateScriptLauncherFromApi(opts: { id: string; title: string; action: string; icon?: string; shortcut?: string }) {
|
||||
if (opts.id && !/^[a-z0-9]+$/i.test(opts.id)) {
|
||||
throw new Error(`Launcher ID can be alphanumeric only, '${opts.id}' given`);
|
||||
}
|
||||
|
||||
const launcherId = opts.id || (`tb_${opts.title.toLowerCase().replace(/[^[a-z0-9]/gi, "")}`);
|
||||
const launcherId = opts.id || `tb_${opts.title.toLowerCase().replace(/[^[a-z0-9]/gi, "")}`;
|
||||
|
||||
if (!opts.title) {
|
||||
throw new Error("Title is mandatory property to create or update a launcher.");
|
||||
}
|
||||
|
||||
const launcherNote = becca.getNote(launcherId)
|
||||
|| createScriptLauncher('_lbVisibleLaunchers', launcherId);
|
||||
const launcherNote = becca.getNote(launcherId) || createScriptLauncher("_lbVisibleLaunchers", launcherId);
|
||||
|
||||
launcherNote.title = opts.title;
|
||||
launcherNote.setContent(`(${opts.action})()`);
|
||||
launcherNote.setLabel('scriptInLauncherContent'); // there's no target note, the script is in the launcher's content
|
||||
launcherNote.mime = 'application/javascript;env=frontend';
|
||||
launcherNote.setLabel("scriptInLauncherContent"); // there's no target note, the script is in the launcher's content
|
||||
launcherNote.mime = "application/javascript;env=frontend";
|
||||
launcherNote.save();
|
||||
|
||||
if (opts.shortcut) {
|
||||
launcherNote.setLabel('keyboardShortcut', opts.shortcut);
|
||||
launcherNote.setLabel("keyboardShortcut", opts.shortcut);
|
||||
} else {
|
||||
launcherNote.removeLabel('keyboardShortcut');
|
||||
launcherNote.removeLabel("keyboardShortcut");
|
||||
}
|
||||
|
||||
if (opts.icon) {
|
||||
launcherNote.setLabel('iconClass', `bx bx-${opts.icon}`);
|
||||
launcherNote.setLabel("iconClass", `bx bx-${opts.icon}`);
|
||||
} else {
|
||||
launcherNote.removeLabel('iconClass');
|
||||
launcherNote.removeLabel("iconClass");
|
||||
}
|
||||
|
||||
return launcherNote;
|
||||
|
||||
@@ -36,16 +36,15 @@ function rebuildIntegrationTestDatabase() {
|
||||
statementCache = {};
|
||||
}
|
||||
|
||||
|
||||
if (!process.env.TRILIUM_INTEGRATION_TEST) {
|
||||
dbConnection.pragma('journal_mode = WAL');
|
||||
dbConnection.pragma("journal_mode = WAL");
|
||||
}
|
||||
|
||||
const LOG_ALL_QUERIES = false;
|
||||
|
||||
type Params = any;
|
||||
|
||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
|
||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
|
||||
process.on(eventType, () => {
|
||||
if (dbConnection) {
|
||||
// closing connection is especially important to fold -wal file into the main DB file
|
||||
@@ -63,7 +62,7 @@ function insert<T extends {}>(tableName: string, rec: T, replace = false) {
|
||||
}
|
||||
|
||||
const columns = keys.join(", ");
|
||||
const questionMarks = keys.map(p => "?").join(", ");
|
||||
const questionMarks = keys.map((p) => "?").join(", ");
|
||||
|
||||
const query = `INSERT
|
||||
${replace ? "OR REPLACE" : ""} INTO
|
||||
@@ -91,9 +90,9 @@ function upsert<T extends {}>(tableName: string, primaryKey: string, rec: T) {
|
||||
|
||||
const columns = keys.join(", ");
|
||||
|
||||
const questionMarks = keys.map(colName => `@${colName}`).join(", ");
|
||||
const questionMarks = keys.map((colName) => `@${colName}`).join(", ");
|
||||
|
||||
const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", ");
|
||||
const updateMarks = keys.map((colName) => `${colName} = @${colName}`).join(", ");
|
||||
|
||||
const query = `INSERT INTO ${tableName} (${columns}) VALUES (${questionMarks})
|
||||
ON CONFLICT (${primaryKey}) DO UPDATE SET ${updateMarks}`;
|
||||
@@ -116,7 +115,7 @@ function stmt(sql: string) {
|
||||
}
|
||||
|
||||
function getRow<T>(query: string, params: Params = []): T {
|
||||
return wrap(query, s => s.get(params)) as T;
|
||||
return wrap(query, (s) => s.get(params)) as T;
|
||||
}
|
||||
|
||||
function getRowOrNull<T>(query: string, params: Params = []): T | null {
|
||||
@@ -125,11 +124,11 @@ function getRowOrNull<T>(query: string, params: Params = []): T | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (all.length > 0 ? all[0] : null) as (T | null);
|
||||
return (all.length > 0 ? all[0] : null) as T | null;
|
||||
}
|
||||
|
||||
function getValue<T>(query: string, params: Params = []): T {
|
||||
return wrap(query, s => s.pluck().get(params)) as T;
|
||||
return wrap(query, (s) => s.pluck().get(params)) as T;
|
||||
}
|
||||
|
||||
// smaller values can result in better performance due to better usage of statement cache
|
||||
@@ -146,30 +145,28 @@ function getManyRows<T>(query: string, params: Params): T[] {
|
||||
|
||||
let j = 1;
|
||||
for (const param of curParams) {
|
||||
curParamsObj['param' + j++] = param;
|
||||
curParamsObj["param" + j++] = param;
|
||||
}
|
||||
|
||||
let i = 1;
|
||||
const questionMarks = curParams.map(() => ":param" + i++).join(",");
|
||||
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
||||
|
||||
const statement = curParams.length === PARAM_LIMIT
|
||||
? stmt(curQuery)
|
||||
: dbConnection.prepare(curQuery);
|
||||
const statement = curParams.length === PARAM_LIMIT ? stmt(curQuery) : dbConnection.prepare(curQuery);
|
||||
|
||||
const subResults = statement.all(curParamsObj);
|
||||
results = results.concat(subResults);
|
||||
}
|
||||
|
||||
return (results as (T[] | null) || []);
|
||||
return (results as T[] | null) || [];
|
||||
}
|
||||
|
||||
function getRows<T>(query: string, params: Params = []): T[] {
|
||||
return wrap(query, s => s.all(params)) as T[];
|
||||
return wrap(query, (s) => s.all(params)) as T[];
|
||||
}
|
||||
|
||||
function getRawRows<T extends {} | unknown[]>(query: string, params: Params = []): T[] {
|
||||
return (wrap(query, s => s.raw().all(params)) as T[]) || [];
|
||||
return (wrap(query, (s) => s.raw().all(params)) as T[]) || [];
|
||||
}
|
||||
|
||||
function iterateRows<T>(query: string, params: Params = []): IterableIterator<T> {
|
||||
@@ -192,11 +189,11 @@ function getMap<K extends string | number | symbol, V>(query: string, params: Pa
|
||||
}
|
||||
|
||||
function getColumn<T>(query: string, params: Params = []): T[] {
|
||||
return wrap(query, s => s.pluck().all(params)) as T[];
|
||||
return wrap(query, (s) => s.pluck().all(params)) as T[];
|
||||
}
|
||||
|
||||
function execute(query: string, params: Params = []): RunResult {
|
||||
return wrap(query, s => s.run(params)) as RunResult;
|
||||
return wrap(query, (s) => s.run(params)) as RunResult;
|
||||
}
|
||||
|
||||
function executeMany(query: string, params: Params) {
|
||||
@@ -212,7 +209,7 @@ function executeMany(query: string, params: Params) {
|
||||
|
||||
let j = 1;
|
||||
for (const param of curParams) {
|
||||
curParamsObj['param' + j++] = param;
|
||||
curParamsObj["param" + j++] = param;
|
||||
}
|
||||
|
||||
let i = 1;
|
||||
@@ -241,8 +238,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown {
|
||||
|
||||
try {
|
||||
result = func(stmt(query));
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
if (e.message.includes("The database connection is not open")) {
|
||||
// this often happens on killing the app which puts these alerts in front of user
|
||||
// in these cases error should be simply ignored.
|
||||
@@ -259,8 +255,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown {
|
||||
if (milliseconds >= 20 && !cls.isSlowQueryLoggingDisabled()) {
|
||||
if (query.includes("WITH RECURSIVE")) {
|
||||
log.info(`Slow recursive query took ${milliseconds}ms.`);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
|
||||
}
|
||||
}
|
||||
@@ -272,13 +267,13 @@ function transactional<T>(func: (statement: Statement) => T) {
|
||||
try {
|
||||
const ret = (dbConnection.transaction(func) as any).deferred();
|
||||
|
||||
if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
|
||||
if (!dbConnection.inTransaction) {
|
||||
// i.e. transaction was really committed (and not just savepoint released)
|
||||
ws.sendTransactionEntityChangesToAllClients();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
const entityChangeIds = cls.getAndClearEntityChangeIds();
|
||||
|
||||
if (entityChangeIds.length > 0) {
|
||||
@@ -312,7 +307,7 @@ function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
|
||||
}
|
||||
|
||||
// doing it manually to avoid this showing up on the slow query list
|
||||
const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`);
|
||||
const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map((paramId) => `(?)`).join(",")}`);
|
||||
|
||||
s.run(paramIds);
|
||||
}
|
||||
@@ -320,8 +315,7 @@ function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
|
||||
async function copyDatabase(targetFilePath: string) {
|
||||
try {
|
||||
fs.unlinkSync(targetFilePath);
|
||||
} catch (e) {
|
||||
} // unlink throws exception if the file did not exist
|
||||
} catch (e) {} // unlink throws exception if the file did not exist
|
||||
|
||||
await dbConnection.backup(targetFilePath);
|
||||
}
|
||||
@@ -333,8 +327,7 @@ function disableSlowQueryLogging<T>(cb: () => T) {
|
||||
cls.disableSlowQueryLogging(true);
|
||||
|
||||
return cb();
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
cls.disableSlowQueryLogging(orig);
|
||||
}
|
||||
}
|
||||
@@ -345,60 +338,60 @@ export default {
|
||||
replace,
|
||||
|
||||
/**
|
||||
* Get single value from the given query - first column from first returned row.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns single value
|
||||
*/
|
||||
* Get single value from the given query - first column from first returned row.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns single value
|
||||
*/
|
||||
getValue,
|
||||
|
||||
/**
|
||||
* Get first returned row.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns - map of column name to column value
|
||||
*/
|
||||
* Get first returned row.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns - map of column name to column value
|
||||
*/
|
||||
getRow,
|
||||
getRowOrNull,
|
||||
|
||||
/**
|
||||
* Get all returned rows.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns - array of all rows, each row is a map of column name to column value
|
||||
*/
|
||||
* Get all returned rows.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns - array of all rows, each row is a map of column name to column value
|
||||
*/
|
||||
getRows,
|
||||
getRawRows,
|
||||
iterateRows,
|
||||
getManyRows,
|
||||
|
||||
/**
|
||||
* Get a map of first column mapping to second column.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns - map of first column to second column
|
||||
*/
|
||||
* Get a map of first column mapping to second column.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns - map of first column to second column
|
||||
*/
|
||||
getMap,
|
||||
|
||||
/**
|
||||
* Get a first column in an array.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns array of first column of all returned rows
|
||||
*/
|
||||
* Get a first column in an array.
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
* @returns array of first column of all returned rows
|
||||
*/
|
||||
getColumn,
|
||||
|
||||
/**
|
||||
* Execute SQL
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
*/
|
||||
* Execute SQL
|
||||
*
|
||||
* @param query - SQL query with ? used as parameter placeholder
|
||||
* @param params - array of params if needed
|
||||
*/
|
||||
execute,
|
||||
executeMany,
|
||||
executeScript,
|
||||
|
||||
@@ -10,7 +10,7 @@ import TaskContext from "./task_context.js";
|
||||
import migrationService from "./migration.js";
|
||||
import cls from "./cls.js";
|
||||
import config from "./config.js";
|
||||
import { OptionRow } from '../becca/entities/rows.js';
|
||||
import { OptionRow } from "../becca/entities/rows.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import BBranch from "../becca/entities/bbranch.js";
|
||||
import zipImportService from "./import/zip.js";
|
||||
@@ -32,13 +32,12 @@ function isDbInitialized() {
|
||||
|
||||
const initialized = sql.getValue("SELECT value FROM options WHERE name = 'initialized'");
|
||||
|
||||
return initialized === 'true';
|
||||
return initialized === "true";
|
||||
}
|
||||
|
||||
async function initDbConnection() {
|
||||
if (!isDbInitialized()) {
|
||||
log.info(`DB not initialized, please visit setup page` +
|
||||
(isElectron() ? '' : ` - http://[your-server-host]:${port} to see instructions on how to initialize Trilium.`));
|
||||
log.info(`DB not initialized, please visit setup page` + (isElectron() ? "" : ` - http://[your-server-host]:${port} to see instructions on how to initialize Trilium.`));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -73,17 +72,17 @@ async function createInitialDatabase() {
|
||||
log.info("Creating root note ...");
|
||||
|
||||
rootNote = new BNote({
|
||||
noteId: 'root',
|
||||
title: 'root',
|
||||
type: 'text',
|
||||
mime: 'text/html'
|
||||
noteId: "root",
|
||||
title: "root",
|
||||
type: "text",
|
||||
mime: "text/html"
|
||||
}).save();
|
||||
|
||||
rootNote.setContent('');
|
||||
rootNote.setContent("");
|
||||
|
||||
new BBranch({
|
||||
noteId: 'root',
|
||||
parentNoteId: 'none',
|
||||
noteId: "root",
|
||||
parentNoteId: "none",
|
||||
isExpanded: true,
|
||||
notePosition: 10
|
||||
}).save();
|
||||
@@ -96,7 +95,7 @@ async function createInitialDatabase() {
|
||||
|
||||
log.info("Importing demo content ...");
|
||||
|
||||
const dummyTaskContext = new TaskContext("no-progress-reporting", 'import', false);
|
||||
const dummyTaskContext = new TaskContext("no-progress-reporting", "import", false);
|
||||
|
||||
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
|
||||
|
||||
@@ -107,12 +106,15 @@ async function createInitialDatabase() {
|
||||
|
||||
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
|
||||
|
||||
optionService.setOption('openNoteContexts', JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
]));
|
||||
optionService.setOption(
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
log.info("Schema and initial content generated.");
|
||||
@@ -120,7 +122,7 @@ async function createInitialDatabase() {
|
||||
initDbConnection();
|
||||
}
|
||||
|
||||
async function createDatabaseForSync(options: OptionRow[], syncServerHost = '', syncProxy = '') {
|
||||
async function createDatabaseForSync(options: OptionRow[], syncServerHost = "", syncProxy = "") {
|
||||
log.info("Creating database for sync");
|
||||
|
||||
if (isDbInitialized()) {
|
||||
@@ -148,7 +150,7 @@ async function createDatabaseForSync(options: OptionRow[], syncServerHost = '',
|
||||
|
||||
function setDbAsInitialized() {
|
||||
if (!isDbInitialized()) {
|
||||
optionService.setOption('initialized', 'true');
|
||||
optionService.setOption("initialized", "true");
|
||||
|
||||
initDbConnection();
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import ws from "./ws.js";
|
||||
import entityChangesService from "./entity_changes.js";
|
||||
import entityConstructor from "../becca/entity_constructor.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface.js';
|
||||
import { CookieJar, ExecOpts } from './request_interface.js';
|
||||
import { EntityChange, EntityChangeRecord, EntityRow } from "./entity_changes_interface.js";
|
||||
import { CookieJar, ExecOpts } from "./request_interface.js";
|
||||
import setupService from "./setup.js";
|
||||
import consistency_checks from "./consistency_checks.js";
|
||||
import becca_loader from "../becca/becca_loader.js";
|
||||
@@ -29,7 +29,7 @@ let outstandingPullCount = 0;
|
||||
|
||||
interface CheckResponse {
|
||||
maxEntityChangeId: number;
|
||||
entityHashes: Record<string, Record<string, string>>
|
||||
entityHashes: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
interface SyncResponse {
|
||||
@@ -52,7 +52,7 @@ async function sync() {
|
||||
try {
|
||||
return await syncMutexService.doExclusively(async () => {
|
||||
if (!syncOptions.isSyncSetup()) {
|
||||
return { success: false, errorCode: 'NOT_CONFIGURED', message: 'Sync not configured' };
|
||||
return { success: false, errorCode: "NOT_CONFIGURED", message: "Sync not configured" };
|
||||
}
|
||||
|
||||
let continueSync = false;
|
||||
@@ -69,8 +69,7 @@ async function sync() {
|
||||
await syncFinished(syncContext);
|
||||
|
||||
continueSync = await checkContentHash(syncContext);
|
||||
}
|
||||
while (continueSync);
|
||||
} while (continueSync);
|
||||
|
||||
ws.syncFinished();
|
||||
|
||||
@@ -78,15 +77,15 @@ async function sync() {
|
||||
success: true
|
||||
};
|
||||
});
|
||||
}
|
||||
catch (e: any) {
|
||||
} catch (e: any) {
|
||||
// we're dynamically switching whether we're using proxy or not based on whether we encountered error with the current method
|
||||
proxyToggle = !proxyToggle;
|
||||
|
||||
if (e.message?.includes('ECONNREFUSED') ||
|
||||
e.message?.includes('ERR_') || // node network errors
|
||||
e.message?.includes('Bad Gateway')) {
|
||||
|
||||
if (
|
||||
e.message?.includes("ECONNREFUSED") ||
|
||||
e.message?.includes("ERR_") || // node network errors
|
||||
e.message?.includes("Bad Gateway")
|
||||
) {
|
||||
ws.syncFailed();
|
||||
|
||||
log.info("No connection to sync server.");
|
||||
@@ -95,8 +94,7 @@ async function sync() {
|
||||
success: false,
|
||||
message: "No connection to sync server."
|
||||
};
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
log.info(`Sync failed: '${e.message}', stack: ${e.stack}`);
|
||||
|
||||
ws.syncFailed();
|
||||
@@ -104,13 +102,13 @@ async function sync() {
|
||||
return {
|
||||
success: false,
|
||||
message: e.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (!await setupService.hasSyncServerSchemaAndSeed()) {
|
||||
if (!(await setupService.hasSyncServerSchemaAndSeed())) {
|
||||
await setupService.sendSeedToSyncServer();
|
||||
}
|
||||
|
||||
@@ -120,11 +118,11 @@ async function login() {
|
||||
async function doLogin(): Promise<SyncContext> {
|
||||
const timestamp = dateUtils.utcNowDateTime();
|
||||
|
||||
const documentSecret = optionService.getOption('documentSecret');
|
||||
const documentSecret = optionService.getOption("documentSecret");
|
||||
const hash = hmac(documentSecret, timestamp);
|
||||
|
||||
const syncContext: SyncContext = { cookieJar: {} };
|
||||
const resp = await syncRequest<SyncResponse>(syncContext, 'POST', '/api/login/sync', {
|
||||
const resp = await syncRequest<SyncResponse>(syncContext, "POST", "/api/login/sync", {
|
||||
timestamp: timestamp,
|
||||
syncVersion: appInfo.syncVersion,
|
||||
hash: hash
|
||||
@@ -135,7 +133,9 @@ async function doLogin(): Promise<SyncContext> {
|
||||
}
|
||||
|
||||
if (resp.instanceId === instanceId) {
|
||||
throw new Error(`Sync server has instance ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
|
||||
throw new Error(
|
||||
`Sync server has instance ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`
|
||||
);
|
||||
}
|
||||
|
||||
syncContext.instanceId = resp.instanceId;
|
||||
@@ -161,11 +161,11 @@ async function pullChanges(syncContext: SyncContext) {
|
||||
|
||||
const startDate = Date.now();
|
||||
|
||||
const resp = await syncRequest<ChangesResponse>(syncContext, 'GET', changesUri);
|
||||
const resp = await syncRequest<ChangesResponse>(syncContext, "GET", changesUri);
|
||||
if (!resp) {
|
||||
throw new Error("Request failed.");
|
||||
}
|
||||
const {entityChanges, lastEntityChangeId} = resp;
|
||||
const { entityChanges, lastEntityChangeId } = resp;
|
||||
|
||||
outstandingPullCount = resp.outstandingPullCount;
|
||||
|
||||
@@ -184,12 +184,14 @@ async function pullChanges(syncContext: SyncContext) {
|
||||
if (entityChanges.length === 0) {
|
||||
break;
|
||||
} else {
|
||||
try { // https://github.com/zadam/trilium/issues/4310
|
||||
try {
|
||||
// https://github.com/zadam/trilium/issues/4310
|
||||
const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
|
||||
|
||||
log.info(`Sync ${logMarkerId}: Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
|
||||
}
|
||||
catch (e: any) {
|
||||
log.info(
|
||||
`Sync ${logMarkerId}: Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`
|
||||
);
|
||||
} catch (e: any) {
|
||||
log.error(`Error occurred ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
@@ -202,7 +204,7 @@ async function pushChanges(syncContext: SyncContext) {
|
||||
let lastSyncedPush: number | null | undefined = getLastSyncedPush();
|
||||
|
||||
while (true) {
|
||||
const entityChanges = sql.getRows<EntityChange>('SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000', [lastSyncedPush]);
|
||||
const entityChanges = sql.getRows<EntityChange>("SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastSyncedPush]);
|
||||
|
||||
if (entityChanges.length === 0) {
|
||||
log.info("Nothing to push");
|
||||
@@ -210,15 +212,14 @@ async function pushChanges(syncContext: SyncContext) {
|
||||
break;
|
||||
}
|
||||
|
||||
const filteredEntityChanges = entityChanges.filter(entityChange => {
|
||||
const filteredEntityChanges = entityChanges.filter((entityChange) => {
|
||||
if (entityChange.instanceId === syncContext.instanceId) {
|
||||
// this may set lastSyncedPush beyond what's actually sent (because of size limit)
|
||||
// so this is applied to the database only if there's no actual update
|
||||
lastSyncedPush = entityChange.id;
|
||||
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -236,7 +237,7 @@ async function pushChanges(syncContext: SyncContext) {
|
||||
|
||||
const logMarkerId = randomString(10); // to easily pair sync events between client and server logs
|
||||
|
||||
await syncRequest(syncContext, 'PUT', `/api/sync/update?logMarkerId=${logMarkerId}`, {
|
||||
await syncRequest(syncContext, "PUT", `/api/sync/update?logMarkerId=${logMarkerId}`, {
|
||||
entities: entityChangesRecords,
|
||||
instanceId
|
||||
});
|
||||
@@ -254,11 +255,11 @@ async function pushChanges(syncContext: SyncContext) {
|
||||
}
|
||||
|
||||
async function syncFinished(syncContext: SyncContext) {
|
||||
await syncRequest(syncContext, 'POST', '/api/sync/finished');
|
||||
await syncRequest(syncContext, "POST", "/api/sync/finished");
|
||||
}
|
||||
|
||||
async function checkContentHash(syncContext: SyncContext) {
|
||||
const resp = await syncRequest<CheckResponse>(syncContext, 'GET', '/api/sync/check');
|
||||
const resp = await syncRequest<CheckResponse>(syncContext, "GET", "/api/sync/check");
|
||||
if (!resp) {
|
||||
throw new Error("Got no response.");
|
||||
}
|
||||
@@ -285,13 +286,13 @@ async function checkContentHash(syncContext: SyncContext) {
|
||||
// before re-queuing sectors, make sure the entity changes are correct
|
||||
consistency_checks.runEntityChangesChecks();
|
||||
|
||||
await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`);
|
||||
await syncRequest(syncContext, "POST", `/api/sync/check-entity-changes`);
|
||||
}
|
||||
|
||||
for (const {entityName, sector} of failedChecks) {
|
||||
for (const { entityName, sector } of failedChecks) {
|
||||
entityChangesService.addEntityChangesForSector(entityName, sector);
|
||||
|
||||
await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`);
|
||||
await syncRequest(syncContext, "POST", `/api/sync/queue-sector/${entityName}/${sector}`);
|
||||
}
|
||||
|
||||
return failedChecks.length > 0;
|
||||
@@ -300,11 +301,11 @@ async function checkContentHash(syncContext: SyncContext) {
|
||||
const PAGE_SIZE = 1000000;
|
||||
|
||||
interface SyncContext {
|
||||
cookieJar: CookieJar
|
||||
cookieJar: CookieJar;
|
||||
}
|
||||
|
||||
async function syncRequest<T extends {}>(syncContext: SyncContext, method: string, requestPath: string, _body?: {}) {
|
||||
const body = _body ? JSON.stringify(_body) : '';
|
||||
const body = _body ? JSON.stringify(_body) : "";
|
||||
|
||||
const timeout = syncOptions.getSyncTimeout();
|
||||
|
||||
@@ -328,19 +329,18 @@ async function syncRequest<T extends {}>(syncContext: SyncContext, method: strin
|
||||
proxy: proxyToggle ? syncOptions.getSyncProxy() : null
|
||||
};
|
||||
|
||||
response = await timeLimit(request.exec(opts), timeout) as T;
|
||||
response = (await timeLimit(request.exec(opts), timeout)) as T;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function getEntityChangeRow(entityChange: EntityChange) {
|
||||
const {entityName, entityId} = entityChange;
|
||||
const { entityName, entityId } = entityChange;
|
||||
|
||||
if (entityName === 'note_reordering') {
|
||||
if (entityName === "note_reordering") {
|
||||
return sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const primaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
|
||||
|
||||
if (!primaryKey) {
|
||||
@@ -354,9 +354,9 @@ function getEntityChangeRow(entityChange: EntityChange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entityName === 'blobs' && entityRow.content !== null) {
|
||||
if (typeof entityRow.content === 'string') {
|
||||
entityRow.content = Buffer.from(entityRow.content, 'utf-8');
|
||||
if (entityName === "blobs" && entityRow.content !== null) {
|
||||
if (typeof entityRow.content === "string") {
|
||||
entityRow.content = Buffer.from(entityRow.content, "utf-8");
|
||||
}
|
||||
|
||||
if (entityRow.content) {
|
||||
@@ -374,7 +374,7 @@ function getEntityChangeRecords(entityChanges: EntityChange[]) {
|
||||
|
||||
for (const entityChange of entityChanges) {
|
||||
if (entityChange.isErased) {
|
||||
records.push({entityChange});
|
||||
records.push({ entityChange });
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -400,22 +400,23 @@ function getEntityChangeRecords(entityChanges: EntityChange[]) {
|
||||
}
|
||||
|
||||
function getLastSyncedPull() {
|
||||
return parseInt(optionService.getOption('lastSyncedPull'));
|
||||
return parseInt(optionService.getOption("lastSyncedPull"));
|
||||
}
|
||||
|
||||
function setLastSyncedPull(entityChangeId: number) {
|
||||
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
|
||||
const lastSyncedPullOption = becca.getOption("lastSyncedPull");
|
||||
|
||||
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
|
||||
if (lastSyncedPullOption) {
|
||||
// might be null in initial sync when becca is not loaded
|
||||
lastSyncedPullOption.value = `${entityChangeId}`;
|
||||
}
|
||||
|
||||
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']);
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, "lastSyncedPull"]);
|
||||
}
|
||||
|
||||
function getLastSyncedPush() {
|
||||
const lastSyncedPush = parseInt(optionService.getOption('lastSyncedPush'));
|
||||
const lastSyncedPush = parseInt(optionService.getOption("lastSyncedPush"));
|
||||
|
||||
ws.setLastSyncedPush(lastSyncedPush);
|
||||
|
||||
@@ -425,18 +426,19 @@ function getLastSyncedPush() {
|
||||
function setLastSyncedPush(entityChangeId: number) {
|
||||
ws.setLastSyncedPush(entityChangeId);
|
||||
|
||||
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
|
||||
const lastSyncedPushOption = becca.getOption("lastSyncedPush");
|
||||
|
||||
if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded
|
||||
if (lastSyncedPushOption) {
|
||||
// might be null in initial sync when becca is not loaded
|
||||
lastSyncedPushOption.value = `${entityChangeId}`;
|
||||
}
|
||||
|
||||
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']);
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, "lastSyncedPush"]);
|
||||
}
|
||||
|
||||
function getMaxEntityChangeId() {
|
||||
return sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes');
|
||||
return sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes");
|
||||
}
|
||||
|
||||
function getOutstandingPullCount() {
|
||||
|
||||
@@ -11,8 +11,7 @@ async function doExclusively<T>(func: () => T) {
|
||||
|
||||
try {
|
||||
return await func();
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
releaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,20 +12,20 @@ import config from "./config.js";
|
||||
*/
|
||||
|
||||
function get(name: OptionNames) {
|
||||
return (config['Sync'] && config['Sync'][name]) || optionService.getOption(name);
|
||||
return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
|
||||
}
|
||||
|
||||
export default {
|
||||
// env variable is the easiest way to guarantee we won't overwrite prod data during development
|
||||
// after copying prod document/data directory
|
||||
getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get('syncServerHost'),
|
||||
getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get("syncServerHost"),
|
||||
isSyncSetup: () => {
|
||||
const syncServerHost = get('syncServerHost');
|
||||
const syncServerHost = get("syncServerHost");
|
||||
|
||||
// special value "disabled" is here to support a use case where the document is configured with sync server,
|
||||
// and we need to override it with config from config.ini
|
||||
return !!syncServerHost && syncServerHost !== 'disabled';
|
||||
return !!syncServerHost && syncServerHost !== "disabled";
|
||||
},
|
||||
getSyncTimeout: () => parseInt(get('syncServerTimeout')) || 120000,
|
||||
getSyncProxy: () => get('syncProxy')
|
||||
getSyncTimeout: () => parseInt(get("syncServerTimeout")) || 120000,
|
||||
getSyncProxy: () => get("syncProxy")
|
||||
};
|
||||
|
||||
@@ -4,12 +4,12 @@ import entityChangesService from "./entity_changes.js";
|
||||
import eventService from "./events.js";
|
||||
import entityConstructor from "../becca/entity_constructor.js";
|
||||
import ws from "./ws.js";
|
||||
import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface.js';
|
||||
import { EntityChange, EntityChangeRecord, EntityRow } from "./entity_changes_interface.js";
|
||||
|
||||
interface UpdateContext {
|
||||
alreadyErased: number;
|
||||
erased: number;
|
||||
updated: Record<string, string[]>
|
||||
updated: Record<string, string[]>;
|
||||
}
|
||||
|
||||
function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string) {
|
||||
@@ -25,9 +25,8 @@ function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string)
|
||||
alreadyErased: 0
|
||||
};
|
||||
|
||||
for (const {entityChange, entity} of entityChanges) {
|
||||
const changeAppliedAlready = entityChange.changeId
|
||||
&& !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
|
||||
for (const { entityChange, entity } of entityChanges) {
|
||||
const changeAppliedAlready = entityChange.changeId && !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
|
||||
|
||||
if (changeAppliedAlready) {
|
||||
updateContext.alreadyUpdated++;
|
||||
@@ -35,7 +34,8 @@ function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!atLeastOnePullApplied) { // avoid spamming and send only for first
|
||||
if (!atLeastOnePullApplied) {
|
||||
// avoid spamming and send only for first
|
||||
ws.syncPullInProgress();
|
||||
|
||||
atLeastOnePullApplied = true;
|
||||
@@ -48,13 +48,11 @@ function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string)
|
||||
}
|
||||
|
||||
function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, instanceId: string, updateContext: UpdateContext) {
|
||||
if (!remoteEntityRow && remoteEC.entityName === 'options') {
|
||||
if (!remoteEntityRow && remoteEC.entityName === "options") {
|
||||
return; // can be undefined for options with isSynced=false
|
||||
}
|
||||
|
||||
const updated = remoteEC.entityName === 'note_reordering'
|
||||
? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
|
||||
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext);
|
||||
const updated = remoteEC.entityName === "note_reordering" ? updateNoteReordering(remoteEC, remoteEntityRow, instanceId) : updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext);
|
||||
|
||||
if (updated) {
|
||||
if (remoteEntityRow?.isDeleted) {
|
||||
@@ -62,8 +60,7 @@ function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undef
|
||||
entityName: remoteEC.entityName,
|
||||
entityId: remoteEC.entityId
|
||||
});
|
||||
}
|
||||
else if (!remoteEC.isErased) {
|
||||
} else if (!remoteEC.isErased) {
|
||||
eventService.emit(eventService.ENTITY_CHANGE_SYNCED, {
|
||||
entityName: remoteEC.entityName,
|
||||
entityRow: remoteEntityRow
|
||||
@@ -74,9 +71,7 @@ function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undef
|
||||
|
||||
function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, instanceId: string, updateContext: UpdateContext) {
|
||||
const localEC = sql.getRow<EntityChange | undefined>(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
|
||||
const localECIsOlderOrSameAsRemote = (
|
||||
localEC && localEC.utcDateChanged && remoteEC.utcDateChanged &&
|
||||
localEC.utcDateChanged <= remoteEC.utcDateChanged);
|
||||
const localECIsOlderOrSameAsRemote = localEC && localEC.utcDateChanged && remoteEC.utcDateChanged && localEC.utcDateChanged <= remoteEC.utcDateChanged;
|
||||
|
||||
if (!localEC || localECIsOlderOrSameAsRemote) {
|
||||
if (remoteEC.isErased) {
|
||||
@@ -100,17 +95,12 @@ function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow |
|
||||
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
|
||||
}
|
||||
|
||||
if (!localEC
|
||||
|| localECIsOlderOrSameAsRemote
|
||||
|| localEC.hash !== remoteEC.hash
|
||||
|| localEC.isErased !== remoteEC.isErased
|
||||
) {
|
||||
if (!localEC || localECIsOlderOrSameAsRemote || localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased) {
|
||||
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased)
|
||||
&& !localECIsOlderOrSameAsRemote) {
|
||||
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased) && !localECIsOlderOrSameAsRemote) {
|
||||
// the change on our side is newer than on the other side, so the other side should update
|
||||
entityChangesService.putEntityChangeForOtherInstances(localEC);
|
||||
|
||||
@@ -121,12 +111,12 @@ function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow |
|
||||
}
|
||||
|
||||
function preProcessContent(remoteEC: EntityChange, remoteEntityRow: EntityRow) {
|
||||
if (remoteEC.entityName === 'blobs' && remoteEntityRow.content !== null) {
|
||||
if (remoteEC.entityName === "blobs" && remoteEntityRow.content !== null) {
|
||||
// we always use a Buffer object which is different from normal saving - there we use a simple string type for
|
||||
// "string notes". The problem is that in general, it's not possible to detect whether a blob content
|
||||
// is string note or note (syncs can arrive out of order)
|
||||
if (typeof remoteEntityRow.content === "string") {
|
||||
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
|
||||
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, "base64");
|
||||
|
||||
if (remoteEntityRow.content.byteLength === 0) {
|
||||
// there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
|
||||
@@ -152,16 +142,9 @@ function updateNoteReordering(remoteEC: EntityChange, remoteEntityRow: EntityRow
|
||||
}
|
||||
|
||||
function eraseEntity(entityChange: EntityChange) {
|
||||
const {entityName, entityId} = entityChange;
|
||||
const { entityName, entityId } = entityChange;
|
||||
|
||||
const entityNames = [
|
||||
"notes",
|
||||
"branches",
|
||||
"attributes",
|
||||
"revisions",
|
||||
"attachments",
|
||||
"blobs"
|
||||
];
|
||||
const entityNames = ["notes", "branches", "attributes", "revisions", "attachments", "blobs"];
|
||||
|
||||
if (!entityNames.includes(entityName)) {
|
||||
log.error(`Cannot erase ${entityName} '${entityId}'.`);
|
||||
@@ -174,10 +157,7 @@ function eraseEntity(entityChange: EntityChange) {
|
||||
}
|
||||
|
||||
function logUpdateContext(updateContext: UpdateContext) {
|
||||
const message = JSON.stringify(updateContext)
|
||||
.replaceAll('"', '')
|
||||
.replaceAll(":", ": ")
|
||||
.replaceAll(",", ", ");
|
||||
const message = JSON.stringify(updateContext).replaceAll('"', "").replaceAll(":", ": ").replaceAll(",", ", ");
|
||||
|
||||
log.info(message.substr(1, message.length - 2));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
import { TaskData } from './task_context_interface.js';
|
||||
import { TaskData } from "./task_context_interface.js";
|
||||
import ws from "./ws.js";
|
||||
|
||||
// taskId => TaskContext
|
||||
const taskContexts: Record<string, TaskContext> = {};
|
||||
|
||||
class TaskContext {
|
||||
|
||||
private taskId: string;
|
||||
private taskType: string | null;
|
||||
private progressCount: number;
|
||||
@@ -43,11 +42,11 @@ class TaskContext {
|
||||
increaseProgressCount() {
|
||||
this.progressCount++;
|
||||
|
||||
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== 'no-progress-reporting') {
|
||||
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== "no-progress-reporting") {
|
||||
this.lastSentCountTs = Date.now();
|
||||
|
||||
ws.sendMessageToAllClients({
|
||||
type: 'taskProgressCount',
|
||||
type: "taskProgressCount",
|
||||
taskId: this.taskId,
|
||||
taskType: this.taskType,
|
||||
data: this.data,
|
||||
@@ -58,7 +57,7 @@ class TaskContext {
|
||||
|
||||
reportError(message: string) {
|
||||
ws.sendMessageToAllClients({
|
||||
type: 'taskError',
|
||||
type: "taskError",
|
||||
taskId: this.taskId,
|
||||
taskType: this.taskType,
|
||||
data: this.data,
|
||||
@@ -68,7 +67,7 @@ class TaskContext {
|
||||
|
||||
taskSucceeded(result?: string | Record<string, string | undefined>) {
|
||||
ws.sendMessageToAllClients({
|
||||
type: 'taskSucceeded',
|
||||
type: "taskSucceeded",
|
||||
taskId: this.taskId,
|
||||
taskType: this.taskType,
|
||||
data: this.data,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Menu, Tray } from 'electron';
|
||||
import { Menu, Tray } from "electron";
|
||||
import path from "path";
|
||||
import windowService from "./window.js";
|
||||
import optionService from "./options.js";
|
||||
@@ -9,56 +9,52 @@ let tray: Tray;
|
||||
// is minimized
|
||||
let isVisible = true;
|
||||
|
||||
|
||||
// Inspired by https://github.com/signalapp/Signal-Desktop/blob/dcb5bb672635c4b29a51adec8a5658e3834ec8fc/app/tray_icon.ts#L20
|
||||
const getIconSize = () => {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
case "darwin":
|
||||
return 16;
|
||||
case 'win32':
|
||||
case "win32":
|
||||
return 32;
|
||||
default:
|
||||
return 256;
|
||||
}
|
||||
}
|
||||
};
|
||||
const getIconPath = () => {
|
||||
const iconSize = getIconSize();
|
||||
|
||||
return path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../..",
|
||||
"images",
|
||||
"app-icons",
|
||||
"png",
|
||||
`${iconSize}x${iconSize}.png`
|
||||
)
|
||||
}
|
||||
return path.join(path.dirname(fileURLToPath(import.meta.url)), "../..", "images", "app-icons", "png", `${iconSize}x${iconSize}.png`);
|
||||
};
|
||||
const registerVisibilityListener = () => {
|
||||
const mainWindow = windowService.getMainWindow();
|
||||
if (!mainWindow) { return; }
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// They need to be registered before the tray updater is registered
|
||||
mainWindow.on('show', () => {
|
||||
mainWindow.on("show", () => {
|
||||
isVisible = true;
|
||||
updateTrayMenu();
|
||||
});
|
||||
mainWindow.on('hide', () => {
|
||||
mainWindow.on("hide", () => {
|
||||
isVisible = false;
|
||||
updateTrayMenu();
|
||||
});
|
||||
|
||||
mainWindow.on("minimize", updateTrayMenu);
|
||||
mainWindow.on("maximize", updateTrayMenu);
|
||||
}
|
||||
};
|
||||
|
||||
const updateTrayMenu = () => {
|
||||
const mainWindow = windowService.getMainWindow();
|
||||
if (!mainWindow) { return; }
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: isVisible ? 'Hide' : 'Show',
|
||||
type: 'normal',
|
||||
label: isVisible ? "Hide" : "Show",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
if (isVisible) {
|
||||
mainWindow.hide();
|
||||
@@ -69,22 +65,24 @@ const updateTrayMenu = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
type: 'normal',
|
||||
label: "Quit",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
mainWindow.close();
|
||||
}
|
||||
},
|
||||
}
|
||||
]);
|
||||
|
||||
tray?.setContextMenu(contextMenu);
|
||||
}
|
||||
};
|
||||
const changeVisibility = () => {
|
||||
const window = windowService.getMainWindow();
|
||||
if (!window) { return; }
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
window.hide();
|
||||
@@ -92,7 +90,7 @@ const changeVisibility = () => {
|
||||
window.show();
|
||||
window.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function createTray() {
|
||||
if (optionService.getOptionBool("disableTray")) {
|
||||
@@ -100,9 +98,9 @@ function createTray() {
|
||||
}
|
||||
|
||||
tray = new Tray(getIconPath());
|
||||
tray.setToolTip('TriliumNext Notes')
|
||||
tray.setToolTip("TriliumNext Notes");
|
||||
// Restore focus
|
||||
tray.on('click', changeVisibility)
|
||||
tray.on("click", changeVisibility);
|
||||
updateTrayMenu();
|
||||
|
||||
registerVisibilityListener();
|
||||
@@ -110,4 +108,4 @@ function createTray() {
|
||||
|
||||
export default {
|
||||
createTray
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user