Merge pull request #25 from TriliumNext/feature/typescript_backend_3

Convert backend to TypeScript (35% -> 50%)
This commit is contained in:
Elian Doran 2024-04-10 19:16:29 +03:00 committed by GitHub
commit 8acfb5b558
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 1056 additions and 724 deletions

View File

@ -5,3 +5,6 @@ cloc HEAD \
--include-lang=javascript,typescript \ --include-lang=javascript,typescript \
--found=filelist.txt \ --found=filelist.txt \
--exclude-dir=public,libraries --exclude-dir=public,libraries
grep -R \.js$ filelist.txt
rm filelist.txt

View File

@ -10,8 +10,8 @@ if (config.Network.https) {
process.exit(0); process.exit(0);
} }
const port = require('./src/services/port.ts'); const port = require('./src/services/port');
const host = require('./src/services/host.js'); const host = require('./src/services/host');
const options = { timeout: 2000 }; const options = { timeout: 2000 };

View File

@ -3,8 +3,8 @@
const {app, globalShortcut, BrowserWindow} = require('electron'); const {app, globalShortcut, BrowserWindow} = require('electron');
const sqlInit = require('./src/services/sql_init'); const sqlInit = require('./src/services/sql_init');
const appIconService = require('./src/services/app_icon.js'); const appIconService = require('./src/services/app_icon.js');
const windowService = require('./src/services/window.js'); const windowService = require('./src/services/window');
const tray = require('./src/services/tray.js'); const tray = require('./src/services/tray');
// Adds debug features like hotkeys for triggering dev tools and reload // Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')(); require('electron-debug')();

View File

@ -1,4 +1,4 @@
const lex = require('../../src/services/search/services/lex.js'); const lex = require('../../src/services/search/services/lex');
describe("Lexer fulltext", () => { describe("Lexer fulltext", () => {
it("simple lexing", () => { it("simple lexing", () => {

View File

@ -1,4 +1,4 @@
const handleParens = require('../../src/services/search/services/handle_parens.js'); const handleParens = require('../../src/services/search/services/handle_parens');
describe("Parens handler", () => { describe("Parens handler", () => {
it("handles parens", () => { it("handles parens", () => {

View File

@ -1,5 +1,5 @@
const SearchContext = require('../../src/services/search/search_context.js'); const SearchContext = require('../../src/services/search/search_context');
const parse = require('../../src/services/search/services/parse.js'); const parse = require('../../src/services/search/services/parse');
function tokens(toks, cur = 0) { function tokens(toks, cur = 0) {
return toks.map(arg => { return toks.map(arg => {

View File

@ -1,7 +1,7 @@
const searchService = require('../../src/services/search/services/search.js'); const searchService = require('../../src/services/search/services/search');
const BNote = require('../../src/becca/entities/bnote.js'); const BNote = require('../../src/becca/entities/bnote.js');
const BBranch = require('../../src/becca/entities/bbranch.js'); const BBranch = require('../../src/becca/entities/bbranch.js');
const SearchContext = require('../../src/services/search/search_context.js'); const SearchContext = require('../../src/services/search/search_context');
const dateUtils = require('../../src/services/date_utils'); const dateUtils = require('../../src/services/date_utils');
const becca = require('../../src/becca/becca.js'); const becca = require('../../src/becca/becca.js');
const {NoteBuilder, findNoteByTitle, note} = require('./becca_mocking.js'); const {NoteBuilder, findNoteByTitle, note} = require('./becca_mocking.js');

View File

@ -1,7 +1,7 @@
const {note} = require('./becca_mocking.js'); const {note} = require('./becca_mocking.js');
const ValueExtractor = require('../../src/services/search/value_extractor.js'); const ValueExtractor = require('../../src/services/search/value_extractor');
const becca = require('../../src/becca/becca.js'); const becca = require('../../src/becca/becca.js');
const SearchContext = require('../../src/services/search/search_context.js'); const SearchContext = require('../../src/services/search/search_context');
const dsc = new SearchContext(); const dsc = new SearchContext();

View File

@ -43,7 +43,7 @@ require('./routes/custom.js').register(app);
require('./routes/error_handlers.js').register(app); require('./routes/error_handlers.js').register(app);
// triggers sync timer // triggers sync timer
require('./services/sync.js'); require('./services/sync');
// triggers backup timer // triggers backup timer
require('./services/backup'); require('./services/backup');

View File

@ -20,7 +20,7 @@ const beccaLoaded = new Promise<void>((res, rej) => {
cls.init(() => { cls.init(() => {
load(); load();
require('../services/options_init.js').initStartupOptions(); require('../services/options_init').initStartupOptions();
res(); res();
}); });

View File

@ -222,7 +222,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
}; };
} }
createClone(type: AttributeType, name: string, value: string, isInheritable: boolean) { createClone(type: AttributeType, name: string, value: string, isInheritable?: boolean) {
return new BAttribute({ return new BAttribute({
noteId: this.noteId, noteId: this.noteId,
type: type, type: type,

View File

@ -267,17 +267,19 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
}; };
} }
createClone(parentNoteId: string, notePosition: number) { createClone(parentNoteId: string, notePosition?: number) {
const existingBranch = this.becca.getBranchFromChildAndParent(this.noteId, parentNoteId); const existingBranch = this.becca.getBranchFromChildAndParent(this.noteId, parentNoteId);
if (existingBranch) { if (existingBranch) {
existingBranch.notePosition = notePosition; if (notePosition) {
existingBranch.notePosition = notePosition;
}
return existingBranch; return existingBranch;
} else { } else {
return new BBranch({ return new BBranch({
noteId: this.noteId, noteId: this.noteId,
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
notePosition: notePosition, notePosition: notePosition || null,
prefix: this.prefix, prefix: this.prefix,
isExpanded: this.isExpanded isExpanded: this.isExpanded
}); });

View File

@ -78,13 +78,13 @@ class BNote extends AbstractBeccaEntity<BNote> {
// following attributes are filled during searching in the database // following attributes are filled during searching in the database
/** size of the content in bytes */ /** size of the content in bytes */
private contentSize!: number | null; contentSize!: number | null;
/** size of the note content, attachment contents in bytes */ /** size of the note content, attachment contents in bytes */
private contentAndAttachmentsSize!: number | null; contentAndAttachmentsSize!: number | null;
/** size of the note content, attachment contents and revision contents in bytes */ /** size of the note content, attachment contents and revision contents in bytes */
private contentAndAttachmentsAndRevisionsSize!: number | null; contentAndAttachmentsAndRevisionsSize!: number | null;
/** number of note revisions for this note */ /** number of note revisions for this note */
private revisionCount!: number | null; revisionCount!: number | null;
constructor(row?: Partial<NoteRow>) { constructor(row?: Partial<NoteRow>) {
super(); super();
@ -450,7 +450,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
); );
} }
getAttributeCaseInsensitive(type: string, name: string, value: string | null) { getAttributeCaseInsensitive(type: string, name: string, value?: string | null) {
name = name.toLowerCase(); name = name.toLowerCase();
value = value ? value.toLowerCase() : null; value = value ? value.toLowerCase() : null;

View File

@ -66,7 +66,7 @@ export type AttributeType = "label" | "relation";
export interface AttributeRow { export interface AttributeRow {
attributeId?: string; attributeId?: string;
noteId: string; noteId?: string;
type: AttributeType; type: AttributeType;
name: string; name: string;
position?: number; position?: number;
@ -80,7 +80,7 @@ export interface BranchRow {
noteId: string; noteId: string;
parentNoteId: string; parentNoteId: string;
prefix?: string | null; prefix?: string | null;
notePosition: number | null; notePosition?: number | null;
isExpanded?: boolean; isExpanded?: boolean;
isDeleted?: boolean; isDeleted?: boolean;
utcDateModified?: string; utcDateModified?: string;
@ -106,4 +106,5 @@ export interface NoteRow {
dateModified: string; dateModified: string;
utcDateCreated: string; utcDateCreated: string;
utcDateModified: string; utcDateModified: string;
content?: string;
} }

View File

@ -1,7 +1,7 @@
const becca = require('../becca/becca'); const becca = require('../becca/becca');
const eu = require('./etapi_utils'); const eu = require('./etapi_utils');
const mappers = require('./mappers.js'); const mappers = require('./mappers.js');
const attributeService = require('../services/attributes.js'); const attributeService = require('../services/attributes');
const v = require('./validators.js'); const v = require('./validators.js');
function register(router) { function register(router) {

View File

@ -5,8 +5,8 @@ const mappers = require('./mappers.js');
const noteService = require('../services/notes'); const noteService = require('../services/notes');
const TaskContext = require('../services/task_context'); const TaskContext = require('../services/task_context');
const v = require('./validators.js'); const v = require('./validators.js');
const searchService = require('../services/search/services/search.js'); const searchService = require('../services/search/services/search');
const SearchContext = require('../services/search/search_context.js'); const SearchContext = require('../services/search/search_context');
const zipExportService = require('../services/export/zip.js'); const zipExportService = require('../services/export/zip.js');
const zipImportService = require('../services/import/zip.js'); const zipImportService = require('../services/import/zip.js');

View File

@ -1,5 +1,5 @@
const specialNotesService = require('../services/special_notes.js'); const specialNotesService = require('../services/special_notes.js');
const dateNotesService = require('../services/date_notes.js'); const dateNotesService = require('../services/date_notes');
const eu = require('./etapi_utils'); const eu = require('./etapi_utils');
const mappers = require('./mappers.js'); const mappers = require('./mappers.js');

View File

@ -2,7 +2,7 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const log = require('../../services/log'); const log = require('../../services/log');
const attributeService = require('../../services/attributes.js'); const attributeService = require('../../services/attributes');
const BAttribute = require('../../becca/entities/battribute'); const BAttribute = require('../../becca/entities/battribute');
const becca = require('../../becca/becca'); const becca = require('../../becca/becca');
const ValidationError = require('../../errors/validation_error'); const ValidationError = require('../../errors/validation_error');

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
const beccaService = require('../../becca/becca_service'); const beccaService = require('../../becca/becca_service');
const searchService = require('../../services/search/services/search.js'); const searchService = require('../../services/search/services/search');
const log = require('../../services/log'); const log = require('../../services/log');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const cls = require('../../services/cls'); const cls = require('../../services/cls');

View File

@ -3,11 +3,11 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const entityChangesService = require('../../services/entity_changes'); const entityChangesService = require('../../services/entity_changes');
const treeService = require('../../services/tree.js'); const treeService = require('../../services/tree');
const eraseService = require('../../services/erase'); const eraseService = require('../../services/erase');
const becca = require('../../becca/becca'); const becca = require('../../becca/becca');
const TaskContext = require('../../services/task_context'); const TaskContext = require('../../services/task_context');
const branchService = require('../../services/branches.js'); const branchService = require('../../services/branches');
const log = require('../../services/log'); const log = require('../../services/log');
const ValidationError = require('../../errors/validation_error'); const ValidationError = require('../../errors/validation_error');
const eventService = require("../../services/events"); const eventService = require("../../services/events");

View File

@ -1,5 +1,5 @@
const becca = require('../../becca/becca'); const becca = require('../../becca/becca');
const bulkActionService = require('../../services/bulk_actions.js'); const bulkActionService = require('../../services/bulk_actions');
function execute(req) { function execute(req) {
const {noteIds, includeDescendants} = req.body; const {noteIds, includeDescendants} = req.body;

View File

@ -1,9 +1,9 @@
"use strict"; "use strict";
const attributeService = require('../../services/attributes.js'); const attributeService = require('../../services/attributes');
const cloneService = require('../../services/cloning.js'); const cloneService = require('../../services/cloning');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const dateNoteService = require('../../services/date_notes.js'); const dateNoteService = require('../../services/date_notes');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const imageService = require('../../services/image.js'); const imageService = require('../../services/image.js');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');

View File

@ -1,6 +1,6 @@
"use strict"; "use strict";
const cloningService = require('../../services/cloning.js'); const cloningService = require('../../services/cloning');
function cloneNoteToBranch(req) { function cloneNoteToBranch(req) {
const {noteId, parentBranchId} = req.params; const {noteId, parentBranchId} = req.params;

View File

@ -2,7 +2,7 @@
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const eraseService = require('../../services/erase'); const eraseService = require('../../services/erase');
const treeService = require('../../services/tree.js'); const treeService = require('../../services/tree');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const log = require('../../services/log'); const log = require('../../services/log');

View File

@ -2,7 +2,7 @@
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const log = require('../../services/log'); const log = require('../../services/log');
const searchService = require('../../services/search/services/search.js'); const searchService = require('../../services/search/services/search');
const ValidationError = require('../../errors/validation_error'); const ValidationError = require('../../errors/validation_error');
// options allowed to be updated directly in the Options dialog // options allowed to be updated directly in the Options dialog

View File

@ -1,9 +1,9 @@
"use strict"; "use strict";
const scriptService = require('../../services/script.js'); const scriptService = require('../../services/script.js');
const attributeService = require('../../services/attributes.js'); const attributeService = require('../../services/attributes');
const becca = require('../../becca/becca'); const becca = require('../../becca/becca');
const syncService = require('../../services/sync.js'); const syncService = require('../../services/sync');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
// The async/await here is very confusing, because the body.script may, but may not be async. If it is async, then we // The async/await here is very confusing, because the body.script may, but may not be async. If it is async, then we

View File

@ -1,9 +1,9 @@
"use strict"; "use strict";
const becca = require('../../becca/becca'); const becca = require('../../becca/becca');
const SearchContext = require('../../services/search/search_context.js'); const SearchContext = require('../../services/search/search_context');
const searchService = require('../../services/search/services/search.js'); const searchService = require('../../services/search/services/search');
const bulkActionService = require('../../services/bulk_actions.js'); const bulkActionService = require('../../services/bulk_actions');
const cls = require('../../services/cls'); const cls = require('../../services/cls');
const {formatAttrForSearch} = require('../../services/attribute_formatter'); const {formatAttrForSearch} = require('../../services/attribute_formatter');
const ValidationError = require('../../errors/validation_error'); const ValidationError = require('../../errors/validation_error');

View File

@ -1,6 +1,6 @@
"use strict"; "use strict";
const dateNoteService = require('../../services/date_notes.js'); const dateNoteService = require('../../services/date_notes');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const cls = require('../../services/cls'); const cls = require('../../services/cls');
const specialNotesService = require('../../services/special_notes.js'); const specialNotesService = require('../../services/special_notes.js');

View File

@ -1,12 +1,12 @@
"use strict"; "use strict";
const syncService = require('../../services/sync.js'); const syncService = require('../../services/sync');
const syncUpdateService = require('../../services/sync_update.js'); const syncUpdateService = require('../../services/sync_update');
const entityChangesService = require('../../services/entity_changes'); const entityChangesService = require('../../services/entity_changes');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const sqlInit = require('../../services/sql_init'); const sqlInit = require('../../services/sql_init');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const contentHashService = require('../../services/content_hash.js'); const contentHashService = require('../../services/content_hash');
const log = require('../../services/log'); const log = require('../../services/log');
const syncOptions = require('../../services/sync_options'); const syncOptions = require('../../services/sync_options');
const utils = require('../../services/utils'); const utils = require('../../services/utils');

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
const sql = require('../services/sql'); const sql = require('../services/sql');
const attributeService = require('../services/attributes.js'); const attributeService = require('../services/attributes');
const config = require('../services/config'); const config = require('../services/config');
const optionService = require('../services/options'); const optionService = require('../services/options');
const log = require('../services/log'); const log = require('../services/log');

View File

@ -27,12 +27,12 @@ const notesApiRoute = require('./api/notes.js');
const branchesApiRoute = require('./api/branches.js'); const branchesApiRoute = require('./api/branches.js');
const attachmentsApiRoute = require('./api/attachments.js'); const attachmentsApiRoute = require('./api/attachments.js');
const autocompleteApiRoute = require('./api/autocomplete.js'); const autocompleteApiRoute = require('./api/autocomplete.js');
const cloningApiRoute = require('./api/cloning.js'); const cloningApiRoute = require('./api/cloning');
const revisionsApiRoute = require('./api/revisions'); const revisionsApiRoute = require('./api/revisions');
const recentChangesApiRoute = require('./api/recent_changes.js'); const recentChangesApiRoute = require('./api/recent_changes.js');
const optionsApiRoute = require('./api/options.js'); const optionsApiRoute = require('./api/options.js');
const passwordApiRoute = require('./api/password'); const passwordApiRoute = require('./api/password');
const syncApiRoute = require('./api/sync.js'); const syncApiRoute = require('./api/sync');
const loginApiRoute = require('./api/login.js'); const loginApiRoute = require('./api/login.js');
const recentNotesRoute = require('./api/recent_notes.js'); const recentNotesRoute = require('./api/recent_notes.js');
const appInfoRoute = require('./api/app_info'); const appInfoRoute = require('./api/app_info');
@ -42,11 +42,11 @@ const setupApiRoute = require('./api/setup.js');
const sqlRoute = require('./api/sql'); const sqlRoute = require('./api/sql');
const databaseRoute = require('./api/database.js'); const databaseRoute = require('./api/database.js');
const imageRoute = require('./api/image.js'); const imageRoute = require('./api/image.js');
const attributesRoute = require('./api/attributes.js'); const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script.js'); const scriptRoute = require('./api/script.js');
const senderRoute = require('./api/sender.js'); const senderRoute = require('./api/sender.js');
const filesRoute = require('./api/files.js'); const filesRoute = require('./api/files.js');
const searchRoute = require('./api/search.js'); const searchRoute = require('./api/search');
const bulkActionRoute = require('./api/bulk_action.js'); const bulkActionRoute = require('./api/bulk_action.js');
const specialNotesRoute = require('./api/special_notes.js'); const specialNotesRoute = require('./api/special_notes.js');
const noteMapRoute = require('./api/note_map.js'); const noteMapRoute = require('./api/note_map.js');
@ -64,7 +64,7 @@ const shareRoutes = require('../share/routes.js');
const etapiAuthRoutes = require('../etapi/auth.js'); const etapiAuthRoutes = require('../etapi/auth.js');
const etapiAppInfoRoutes = require('../etapi/app_info'); const etapiAppInfoRoutes = require('../etapi/app_info');
const etapiAttachmentRoutes = require('../etapi/attachments.js'); const etapiAttachmentRoutes = require('../etapi/attachments.js');
const etapiAttributeRoutes = require('../etapi/attributes.js'); const etapiAttributeRoutes = require('../etapi/attributes');
const etapiBranchRoutes = require('../etapi/branches.js'); const etapiBranchRoutes = require('../etapi/branches.js');
const etapiNoteRoutes = require('../etapi/notes.js'); const etapiNoteRoutes = require('../etapi/notes.js');
const etapiSpecialNoteRoutes = require('../etapi/special_notes.js'); const etapiSpecialNoteRoutes = require('../etapi/special_notes.js');

View File

@ -1,5 +1,5 @@
const session = require("express-session"); const session = require("express-session");
const sessionSecret = require('../services/session_secret.js'); const sessionSecret = require('../services/session_secret');
const dataDir = require('../services/data_dir'); const dataDir = require('../services/data_dir');
const FileStore = require('session-file-store')(session); const FileStore = require('session-file-store')(session);

View File

@ -9,7 +9,7 @@ const appPath = require('../services/app_path');
function setupPage(req, res) { function setupPage(req, res) {
if (sqlInit.isDbInitialized()) { if (sqlInit.isDbInitialized()) {
if (utils.isElectron()) { if (utils.isElectron()) {
const windowService = require('../services/window.js'); const windowService = require('../services/window');
const {app} = require('electron'); const {app} = require('electron');
windowService.createMainWindow(app); windowService.createMainWindow(app);
windowService.closeSetupWindow(); windowService.closeSetupWindow();

View File

@ -1,8 +1,8 @@
"use strict"; "use strict";
import BAttribute = require("../becca/entities/battribute"); import { AttributeRow } from "../becca/entities/rows";
function formatAttrForSearch(attr: BAttribute, searchWithValue: string) { function formatAttrForSearch(attr: AttributeRow, searchWithValue: boolean) {
let searchStr = ''; let searchStr = '';
if (attr.type === 'label') { if (attr.type === 'label') {

View File

@ -1,17 +1,18 @@
"use strict"; "use strict";
const searchService = require('./search/services/search.js'); import searchService = require('./search/services/search');
const sql = require('./sql'); import sql = require('./sql');
const becca = require('../becca/becca'); import becca = require('../becca/becca');
const BAttribute = require('../becca/entities/battribute'); import BAttribute = require('../becca/entities/battribute');
const {formatAttrForSearch} = require('./attribute_formatter'); import attributeFormatter = require('./attribute_formatter');
const BUILTIN_ATTRIBUTES = require('./builtin_attributes'); import BUILTIN_ATTRIBUTES = require('./builtin_attributes');
import BNote = require('../becca/entities/bnote');
import { AttributeRow } from '../becca/entities/rows';
const ATTRIBUTE_TYPES = ['label', 'relation']; const ATTRIBUTE_TYPES = ['label', 'relation'];
/** @returns {BNote[]} */ function getNotesWithLabel(name: string, value?: string): BNote[] {
function getNotesWithLabel(name, value = undefined) { const query = attributeFormatter.formatAttrForSearch({type: 'label', name, value}, value !== undefined);
const query = formatAttrForSearch({type: 'label', name, value}, value !== undefined);
return searchService.searchNotes(query, { return searchService.searchNotes(query, {
includeArchivedNotes: true, includeArchivedNotes: true,
ignoreHoistedNote: true ignoreHoistedNote: true
@ -19,8 +20,7 @@ function getNotesWithLabel(name, value = undefined) {
} }
// TODO: should be in search service // TODO: should be in search service
/** @returns {BNote|null} */ function getNoteWithLabel(name: string, value?: string): BNote | null {
function getNoteWithLabel(name, value = undefined) {
// optimized version (~20 times faster) without using normal search, useful for e.g., finding date notes // 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);
@ -39,7 +39,7 @@ function getNoteWithLabel(name, value = undefined) {
return null; return null;
} }
function createLabel(noteId, name, value = "") { function createLabel(noteId: string, name: string, value: string = "") {
return createAttribute({ return createAttribute({
noteId: noteId, noteId: noteId,
type: 'label', type: 'label',
@ -48,7 +48,7 @@ function createLabel(noteId, name, value = "") {
}); });
} }
function createRelation(noteId, name, targetNoteId) { function createRelation(noteId: string, name: string, targetNoteId: string) {
return createAttribute({ return createAttribute({
noteId: noteId, noteId: noteId,
type: 'relation', type: 'relation',
@ -57,14 +57,14 @@ function createRelation(noteId, name, targetNoteId) {
}); });
} }
function createAttribute(attribute) { function createAttribute(attribute: AttributeRow) {
return new BAttribute(attribute).save(); return new BAttribute(attribute).save();
} }
function getAttributeNames(type, nameLike) { function getAttributeNames(type: string, nameLike: string) {
nameLike = nameLike.toLowerCase(); nameLike = nameLike.toLowerCase();
let names = sql.getColumn( let names = sql.getColumn<string>(
`SELECT DISTINCT name `SELECT DISTINCT name
FROM attributes FROM attributes
WHERE isDeleted = 0 WHERE isDeleted = 0
@ -98,11 +98,11 @@ function getAttributeNames(type, nameLike) {
return names; return names;
} }
function isAttributeType(type) { function isAttributeType(type: string): boolean {
return ATTRIBUTE_TYPES.includes(type); return ATTRIBUTE_TYPES.includes(type);
} }
function isAttributeDangerous(type, name) { function isAttributeDangerous(type: string, name: string): boolean {
return BUILTIN_ATTRIBUTES.some(attr => return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === type && attr.type === type &&
attr.name.toLowerCase() === name.trim().toLowerCase() && attr.name.toLowerCase() === name.trim().toLowerCase() &&
@ -110,7 +110,7 @@ function isAttributeDangerous(type, name) {
); );
} }
module.exports = { export = {
getNotesWithLabel, getNotesWithLabel,
getNoteWithLabel, getNoteWithLabel,
createLabel, createLabel,

View File

@ -2,22 +2,22 @@ const log = require('./log');
const noteService = require('./notes'); const noteService = require('./notes');
const sql = require('./sql'); const sql = require('./sql');
const utils = require('./utils'); const utils = require('./utils');
const attributeService = require('./attributes.js'); const attributeService = require('./attributes');
const dateNoteService = require('./date_notes.js'); const dateNoteService = require('./date_notes');
const treeService = require('./tree.js'); const treeService = require('./tree');
const config = require('./config'); const config = require('./config');
const axios = require('axios'); const axios = require('axios');
const dayjs = require('dayjs'); const dayjs = require('dayjs');
const xml2js = require('xml2js'); const xml2js = require('xml2js');
const cloningService = require('./cloning.js'); const cloningService = require('./cloning');
const appInfo = require('./app_info'); const appInfo = require('./app_info');
const searchService = require('./search/services/search.js'); const searchService = require('./search/services/search');
const SearchContext = require('./search/search_context.js'); const SearchContext = require('./search/search_context');
const becca = require('../becca/becca'); const becca = require('../becca/becca');
const ws = require('./ws'); const ws = require('./ws');
const SpacedUpdate = require('./spaced_update.js'); const SpacedUpdate = require('./spaced_update.js');
const specialNotesService = require('./special_notes.js'); const specialNotesService = require('./special_notes.js');
const branchService = require('./branches.js'); const branchService = require('./branches');
const exportService = require('./export/zip.js'); const exportService = require('./export/zip.js');
const syncMutex = require('./sync_mutex'); const syncMutex = require('./sync_mutex');
const backupService = require('./backup'); const backupService = require('./backup');

View File

@ -1,7 +1,8 @@
const treeService = require('./tree.js'); import treeService = require('./tree');
const sql = require('./sql'); import sql = require('./sql');
import BBranch = require('../becca/entities/bbranch.js');
function moveBranchToNote(branchToMove, targetParentNoteId) { function moveBranchToNote(branchToMove: BBranch, targetParentNoteId: string) {
if (branchToMove.parentNoteId === targetParentNoteId) { if (branchToMove.parentNoteId === targetParentNoteId) {
return {success: true}; // no-op return {success: true}; // no-op
} }
@ -12,8 +13,8 @@ function moveBranchToNote(branchToMove, targetParentNoteId) {
return [200, validationResult]; return [200, validationResult];
} }
const maxNotePos = sql.getValue('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 === null ? 0 : maxNotePos + 10; const newNotePos = !maxNotePos ? 0 : maxNotePos + 10;
const newBranch = branchToMove.createClone(targetParentNoteId, newNotePos); const newBranch = branchToMove.createClone(targetParentNoteId, newNotePos);
newBranch.save(); newBranch.save();
@ -26,10 +27,10 @@ function moveBranchToNote(branchToMove, targetParentNoteId) {
}; };
} }
function moveBranchToBranch(branchToMove, targetParentBranch) { function moveBranchToBranch(branchToMove: BBranch, targetParentBranch: BBranch) {
const res = moveBranchToNote(branchToMove, targetParentBranch.noteId); const res = moveBranchToNote(branchToMove, targetParentBranch.noteId);
if (!res.success) { if (!("success" in res) || !res.success) {
return res; return res;
} }
@ -42,7 +43,7 @@ function moveBranchToBranch(branchToMove, targetParentBranch) {
return res; return res;
} }
module.exports = { export = {
moveBranchToBranch, moveBranchToBranch,
moveBranchToNote moveBranchToNote
}; };

View File

@ -1,12 +1,30 @@
const log = require('./log'); import log = require('./log');
const revisionService = require('./revisions'); import becca = require('../becca/becca');
const becca = require('../becca/becca'); import cloningService = require('./cloning');
const cloningService = require('./cloning.js'); import branchService = require('./branches');
const branchService = require('./branches.js'); import utils = require('./utils');
const utils = require('./utils'); import eraseService = require("./erase");
const eraseService = require("./erase"); import BNote = require('../becca/entities/bnote');
const ACTION_HANDLERS = { interface Action {
labelName: string;
labelValue: string;
oldLabelName: string;
newLabelName: string;
relationName: string;
oldRelationName: string;
newRelationName: string;
targetNoteId: string;
targetParentNoteId: string;
newTitle: string;
script: string;
}
type ActionHandler = (action: Action, note: BNote) => void;
const ACTION_HANDLERS: Record<string, ActionHandler> = {
addLabel: (action, note) => { addLabel: (action, note) => {
note.addLabel(action.labelName, action.labelValue); note.addLabel(action.labelName, action.labelValue);
}, },
@ -19,7 +37,10 @@ const ACTION_HANDLERS = {
note.deleteNote(deleteId); note.deleteNote(deleteId);
}, },
deleteRevisions: (action, note) => { deleteRevisions: (action, note) => {
eraseService.eraseRevisions(note.getRevisions().map(rev => rev.revisionId)); const revisionIds = note.getRevisions()
.map(rev => rev.revisionId)
.filter((rev) => !!rev) as string[];
eraseService.eraseRevisions(revisionIds);
}, },
deleteLabel: (action, note) => { deleteLabel: (action, note) => {
for (const label of note.getOwnedLabels(action.labelName)) { for (const label of note.getOwnedLabels(action.labelName)) {
@ -107,7 +128,7 @@ const ACTION_HANDLERS = {
} }
}; };
function getActions(note) { function getActions(note: BNote) {
return note.getLabels('action') return note.getLabels('action')
.map(actionLabel => { .map(actionLabel => {
let action; let action;
@ -129,7 +150,7 @@ function getActions(note) {
.filter(a => !!a); .filter(a => !!a);
} }
function executeActions(note, searchResultNoteIds) { function executeActions(note: BNote, searchResultNoteIds: string[]) {
const actions = getActions(note); const actions = getActions(note);
for (const resultNoteId of searchResultNoteIds) { for (const resultNoteId of searchResultNoteIds) {
@ -144,13 +165,13 @@ function executeActions(note, searchResultNoteIds) {
log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`);
ACTION_HANDLERS[action.name](action, resultNote); ACTION_HANDLERS[action.name](action, resultNote);
} catch (e) { } catch (e: any) {
log.error(`ExecuteScript search action failed with ${e.message}`); log.error(`ExecuteScript search action failed with ${e.message}`);
} }
} }
} }
} }
module.exports = { export = {
executeActions executeActions
}; };

View File

@ -2,12 +2,12 @@
const sql = require('./sql'); const sql = require('./sql');
const eventChangesService = require('./entity_changes'); const eventChangesService = require('./entity_changes');
const treeService = require('./tree.js'); const treeService = require('./tree');
const BBranch = require('../becca/entities/bbranch'); const BBranch = require('../becca/entities/bbranch');
const becca = require('../becca/becca'); const becca = require('../becca/becca');
const log = require('./log'); const log = require('./log');
function cloneNoteToParentNote(noteId, parentNoteId, prefix = null) { function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: string | null = null) {
if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) { 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.' };
} }
@ -43,7 +43,7 @@ function cloneNoteToParentNote(noteId, parentNoteId, prefix = null) {
}; };
} }
function cloneNoteToBranch(noteId, parentBranchId, prefix) { function cloneNoteToBranch(noteId: string, parentBranchId: string, prefix: string) {
const parentBranch = becca.getBranch(parentBranchId); const parentBranch = becca.getBranch(parentBranchId);
if (!parentBranch) { if (!parentBranch) {
@ -58,7 +58,7 @@ function cloneNoteToBranch(noteId, parentBranchId, prefix) {
return ret; return ret;
} }
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) { function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefix: string) {
if (!(noteId in becca.notes)) { if (!(noteId in becca.notes)) {
return { branch: null, success: false, message: `Note '${noteId}' is deleted.` }; return { branch: null, success: false, message: `Note '${noteId}' is deleted.` };
} else if (!(parentNoteId in becca.notes)) { } else if (!(parentNoteId in becca.notes)) {
@ -89,7 +89,7 @@ function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
return { branch: branch, success: true }; return { branch: branch, success: true };
} }
function ensureNoteIsAbsentFromParent(noteId, parentNoteId) { function ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string) {
const branchId = sql.getValue(`SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]); const branchId = sql.getValue(`SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]);
const branch = becca.getBranch(branchId); const branch = becca.getBranch(branchId);
@ -109,7 +109,7 @@ function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
} }
} }
function toggleNoteInParent(present, noteId, parentNoteId, prefix) { function toggleNoteInParent(present: boolean, noteId: string, parentNoteId: string, prefix: string) {
if (present) { if (present) {
return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix); return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
} }
@ -118,7 +118,7 @@ function toggleNoteInParent(present, noteId, parentNoteId, prefix) {
} }
} }
function cloneNoteAfter(noteId, afterBranchId) { 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.` }; return { success: false, message: `Cloning the note '${noteId}' is forbidden.` };
} }
@ -175,7 +175,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
return { success: true, branchId: branch.branchId }; return { success: true, branchId: branch.branchId };
} }
module.exports = { export = {
cloneNoteToBranch, cloneNoteToBranch,
cloneNoteToParentNote, cloneNoteToParentNote,
ensureNoteIsPresentInParent, ensureNoteIsPresentInParent,

View File

@ -1,9 +1,11 @@
"use strict"; "use strict";
const sql = require('./sql'); import sql = require('./sql');
const utils = require('./utils'); import utils = require('./utils');
const log = require('./log'); import log = require('./log');
const eraseService = require('./erase'); import eraseService = require('./erase');
type SectorHash = Record<string, string>;
function getEntityHashes() { function getEntityHashes() {
// blob erasure is not synced, we should check before each sync if there's some blob to erase // blob erasure is not synced, we should check before each sync if there's some blob to erase
@ -12,8 +14,9 @@ function getEntityHashes() {
const startTime = new Date(); const startTime = new Date();
// we know this is slow and the total content hash calculation time is logged // we know this is slow and the total content hash calculation time is logged
type HashRow = [ string, string, string, boolean ];
const hashRows = sql.disableSlowQueryLogging( const hashRows = sql.disableSlowQueryLogging(
() => sql.getRawRows(` () => sql.getRawRows<HashRow>(`
SELECT entityName, SELECT entityName,
entityId, entityId,
hash, hash,
@ -27,7 +30,7 @@ function getEntityHashes() {
// sorting by entityId is enough, hashes will be segmented by entityName later on anyway // 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 = {}; const hashMap: Record<string, SectorHash> = {};
for (const [entityName, entityId, hash, isErased] of hashRows) { for (const [entityName, entityId, hash, isErased] of hashRows) {
const entityHashMap = hashMap[entityName] = hashMap[entityName] || {}; const entityHashMap = hashMap[entityName] = hashMap[entityName] || {};
@ -51,13 +54,13 @@ function getEntityHashes() {
return hashMap; return hashMap;
} }
function checkContentHashes(otherHashes) { function checkContentHashes(otherHashes: Record<string, SectorHash>) {
const entityHashes = getEntityHashes(); const entityHashes = getEntityHashes();
const failedChecks = []; const failedChecks = [];
for (const entityName in entityHashes) { for (const entityName in entityHashes) {
const thisSectorHashes = entityHashes[entityName] || {}; const thisSectorHashes: SectorHash = entityHashes[entityName] || {};
const otherSectorHashes = otherHashes[entityName] || {}; const otherSectorHashes: SectorHash = otherHashes[entityName] || {};
const sectors = new Set(Object.keys(thisSectorHashes).concat(Object.keys(otherSectorHashes))); const sectors = new Set(Object.keys(thisSectorHashes).concat(Object.keys(otherSectorHashes)));
@ -77,7 +80,7 @@ function checkContentHashes(otherHashes) {
return failedChecks; return failedChecks;
} }
module.exports = { export = {
getEntityHashes, getEntityHashes,
checkContentHashes checkContentHashes
}; };

View File

@ -1,13 +1,14 @@
"use strict"; "use strict";
const noteService = require('./notes'); import noteService = require('./notes');
const attributeService = require('./attributes.js'); import attributeService = require('./attributes');
const dateUtils = require('./date_utils'); import dateUtils = require('./date_utils');
const sql = require('./sql'); import sql = require('./sql');
const protectedSessionService = require('./protected_session'); import protectedSessionService = require('./protected_session');
const searchService = require('../services/search/services/search.js'); import searchService = require('../services/search/services/search');
const SearchContext = require('../services/search/search_context.js'); import SearchContext = require('../services/search/search_context');
const hoistedNoteService = require('./hoisted_note.js'); import hoistedNoteService = require('./hoisted_note');
import BNote = require('../becca/entities/bnote');
const CALENDAR_ROOT_LABEL = 'calendarRoot'; const CALENDAR_ROOT_LABEL = 'calendarRoot';
const YEAR_LABEL = 'yearNote'; const YEAR_LABEL = 'yearNote';
@ -17,7 +18,9 @@ const DATE_LABEL = 'dateNote';
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']; const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
function createNote(parentNote, noteTitle) { type StartOfWeek = "monday" | "sunday";
function createNote(parentNote: BNote, noteTitle: string) {
return noteService.createNewNote({ return noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title: noteTitle, title: noteTitle,
@ -27,13 +30,12 @@ function createNote(parentNote, noteTitle) {
}).note; }).note;
} }
/** @returns {BNote} */ function getRootCalendarNote(): BNote {
function getRootCalendarNote() {
let rootNote; let rootNote;
const workspaceNote = hoistedNoteService.getWorkspaceNote(); const workspaceNote = hoistedNoteService.getWorkspaceNote();
if (!workspaceNote.isRoot()) { if (!workspaceNote || !workspaceNote.isRoot()) {
rootNote = searchService.findFirstNoteWithQuery('#workspaceCalendarRoot', new SearchContext({ignoreHoistedNote: false})); rootNote = searchService.findFirstNoteWithQuery('#workspaceCalendarRoot', new SearchContext({ignoreHoistedNote: false}));
} }
@ -57,14 +59,11 @@ function getRootCalendarNote() {
}); });
} }
return rootNote; return rootNote as BNote;
} }
/** @returns {BNote} */ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
function getYearNote(dateStr, rootNote = null) { const rootNote = _rootNote || getRootCalendarNote();
if (!rootNote) {
rootNote = getRootCalendarNote();
}
const yearStr = dateStr.trim().substr(0, 4); const yearStr = dateStr.trim().substr(0, 4);
@ -88,10 +87,10 @@ function getYearNote(dateStr, rootNote = null) {
} }
}); });
return yearNote; return yearNote as unknown as BNote;
} }
function getMonthNoteTitle(rootNote, monthNumber, dateObj) { function getMonthNoteTitle(rootNote: BNote, monthNumber: string, dateObj: Date) {
const pattern = rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}"; const pattern = rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}";
const monthName = MONTHS[dateObj.getMonth()]; const monthName = MONTHS[dateObj.getMonth()];
@ -102,11 +101,8 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
.replace(/{month}/g, monthName); .replace(/{month}/g, monthName);
} }
/** @returns {BNote} */ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
function getMonthNote(dateStr, rootNote = null) { const rootNote = _rootNote || getRootCalendarNote();
if (!rootNote) {
rootNote = getRootCalendarNote();
}
const monthStr = dateStr.substr(0, 7); const monthStr = dateStr.substr(0, 7);
const monthNumber = dateStr.substr(5, 2); const monthNumber = dateStr.substr(5, 2);
@ -137,10 +133,10 @@ function getMonthNote(dateStr, rootNote = null) {
} }
}); });
return monthNote; return monthNote as unknown as BNote;
} }
function getDayNoteTitle(rootNote, dayNumber, dateObj) { function getDayNoteTitle(rootNote: BNote, dayNumber: string, dateObj: Date) {
const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}"; const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const weekDay = DAYS[dateObj.getDay()]; const weekDay = DAYS[dateObj.getDay()];
@ -154,18 +150,15 @@ function getDayNoteTitle(rootNote, dayNumber, dateObj) {
} }
/** produces 1st, 2nd, 3rd, 4th, 21st, 31st for 1, 2, 3, 4, 21, 31 */ /** produces 1st, 2nd, 3rd, 4th, 21st, 31st for 1, 2, 3, 4, 21, 31 */
function ordinal(dayNumber) { function ordinal(dayNumber: number) {
const suffixes = ["th", "st", "nd", "rd"]; const suffixes = ["th", "st", "nd", "rd"];
const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0]; const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0];
return `${dayNumber}${suffix}`; return `${dayNumber}${suffix}`;
} }
/** @returns {BNote} */ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
function getDayNote(dateStr, rootNote = null) { const rootNote = _rootNote || getRootCalendarNote();
if (!rootNote) {
rootNote = getRootCalendarNote();
}
dateStr = dateStr.trim().substr(0, 10); dateStr = dateStr.trim().substr(0, 10);
@ -195,14 +188,14 @@ function getDayNote(dateStr, rootNote = null) {
} }
}); });
return dateNote; return dateNote as unknown as BNote;
} }
function getTodayNote(rootNote = null) { function getTodayNote(rootNote = null) {
return getDayNote(dateUtils.localNowDate(), rootNote); return getDayNote(dateUtils.localNowDate(), rootNote);
} }
function getStartOfTheWeek(date, startOfTheWeek) { function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) {
const day = date.getDay(); const day = date.getDay();
let diff; let diff;
@ -219,7 +212,11 @@ function getStartOfTheWeek(date, startOfTheWeek) {
return new Date(date.setDate(diff)); return new Date(date.setDate(diff));
} }
function getWeekNote(dateStr, options = {}, rootNote = null) { interface WeekNoteOpts {
startOfTheWeek?: StartOfWeek
}
function getWeekNote(dateStr: string, options: WeekNoteOpts = {}, rootNote = null) {
const startOfTheWeek = options.startOfTheWeek || "monday"; const startOfTheWeek = options.startOfTheWeek || "monday";
const dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek); const dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek);
@ -229,7 +226,7 @@ function getWeekNote(dateStr, options = {}, rootNote = null) {
return getDayNote(dateStr, rootNote); return getDayNote(dateStr, rootNote);
} }
module.exports = { export = {
getRootCalendarNote, getRootCalendarNote,
getYearNote, getYearNote,
getMonthNote, getMonthNote,

View File

@ -13,3 +13,13 @@ export interface EntityChange {
changeId?: string | null; changeId?: string | null;
instanceId?: string | null; instanceId?: string | null;
} }
export interface EntityRow {
isDeleted?: boolean;
content?: Buffer | string;
}
export interface EntityChangeRecord {
entityChange: EntityChange;
entity?: EntityRow;
}

View File

@ -1,11 +1,11 @@
const eventService = require('./events'); const eventService = require('./events');
const scriptService = require('./script.js'); const scriptService = require('./script.js');
const treeService = require('./tree.js'); const treeService = require('./tree');
const noteService = require('./notes'); const noteService = require('./notes');
const becca = require('../becca/becca'); const becca = require('../becca/becca');
const BAttribute = require('../becca/entities/battribute'); const BAttribute = require('../becca/entities/battribute');
const hiddenSubtreeService = require('./hidden_subtree'); const hiddenSubtreeService = require('./hidden_subtree');
const oneTimeTimer = require('./one_time_timer.js'); const oneTimeTimer = require('./one_time_timer');
function runAttachedRelations(note, relationName, originEntity) { function runAttachedRelations(note, relationName, originEntity) {
if (!note) { if (!note) {

View File

@ -1,5 +1,5 @@
const cls = require('./cls'); import cls = require('./cls');
const becca = require('../becca/becca'); import becca = require('../becca/becca');
function getHoistedNoteId() { function getHoistedNoteId() {
return cls.getHoistedNoteId(); return cls.getHoistedNoteId();
@ -26,14 +26,14 @@ function isHoistedInHiddenSubtree() {
function getWorkspaceNote() { function getWorkspaceNote() {
const hoistedNote = becca.getNote(cls.getHoistedNoteId()); const hoistedNote = becca.getNote(cls.getHoistedNoteId());
if (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace')) { if (hoistedNote && (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace'))) {
return hoistedNote; return hoistedNote;
} else { } else {
return becca.getRoot(); return becca.getRoot();
} }
} }
module.exports = { export = {
getHoistedNoteId, getHoistedNoteId,
getWorkspaceNote, getWorkspaceNote,
isHoistedInHiddenSubtree isHoistedInHiddenSubtree

View File

@ -1,3 +0,0 @@
const config = require('./config');
module.exports = process.env.TRILIUM_HOST || config['Network']['host'] || '0.0.0.0';

3
src/services/host.ts Normal file
View File

@ -0,0 +1,3 @@
import config = require('./config');
export = process.env.TRILIUM_HOST || config['Network']['host'] || '0.0.0.0';

View File

@ -4,12 +4,12 @@ const BAttribute = require('../../becca/entities/battribute');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const log = require('../../services/log'); const log = require('../../services/log');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const attributeService = require('../../services/attributes.js'); const attributeService = require('../../services/attributes');
const BBranch = require('../../becca/entities/bbranch'); const BBranch = require('../../becca/entities/bbranch');
const path = require('path'); const path = require('path');
const protectedSessionService = require('../protected_session'); const protectedSessionService = require('../protected_session');
const mimeService = require('./mime.js'); const mimeService = require('./mime.js');
const treeService = require('../tree.js'); const treeService = require('../tree');
const yauzl = require("yauzl"); const yauzl = require("yauzl");
const htmlSanitizer = require('../html_sanitizer'); const htmlSanitizer = require('../html_sanitizer');
const becca = require('../../becca/becca'); const becca = require('../../becca/becca');

View File

@ -3,19 +3,11 @@
import optionService = require('./options'); import optionService = require('./options');
import log = require('./log'); import log = require('./log');
import utils = require('./utils'); import utils = require('./utils');
import { KeyboardShortcut } from './keyboard_actions_interface';
const isMac = process.platform === "darwin"; const isMac = process.platform === "darwin";
const isElectron = utils.isElectron(); const isElectron = utils.isElectron();
interface KeyboardShortcut {
separator?: string;
actionName?: string;
description?: string;
defaultShortcuts?: string[];
effectiveShortcuts?: string[];
scope?: string;
}
/** /**
* Scope here means on which element the keyboard shortcuts are attached - this means that for the shortcut to work, * 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. * the focus has to be inside the element.

View File

@ -0,0 +1,12 @@
export interface KeyboardShortcut {
separator?: string;
actionName?: string;
description?: string;
defaultShortcuts?: string[];
effectiveShortcuts?: string[];
scope?: string;
}
export interface KeyboardShortcutWithRequiredActionName extends KeyboardShortcut {
actionName: string;
}

View File

@ -173,6 +173,7 @@ interface NoteParams {
dateCreated?: string; dateCreated?: string;
utcDateCreated?: string; utcDateCreated?: string;
ignoreForbiddenParents?: boolean; ignoreForbiddenParents?: boolean;
target?: "into";
} }
function createNewNote(params: NoteParams): { function createNewNote(params: NoteParams): {

View File

@ -1,4 +1,4 @@
const scheduledExecutions = {}; const scheduledExecutions: Record<string, boolean> = {};
/** /**
* Subsequent calls will not move the timer to the future. The first caller determines the time of execution. * Subsequent calls will not move the timer to the future. The first caller determines the time of execution.
@ -6,7 +6,7 @@ const scheduledExecutions = {};
* The good thing about synchronous better-sqlite3 is that this cannot interrupt transaction. The execution will be called * The good thing about synchronous better-sqlite3 is that this cannot interrupt transaction. The execution will be called
* only outside of a transaction. * only outside of a transaction.
*/ */
function scheduleExecution(name, milliseconds, cb) { function scheduleExecution(name: string, milliseconds: number, cb: () => void) {
if (name in scheduledExecutions) { if (name in scheduledExecutions) {
return; return;
} }
@ -20,6 +20,6 @@ function scheduleExecution(name, milliseconds, cb) {
}, milliseconds); }, milliseconds);
} }
module.exports = { export = {
scheduleExecution scheduleExecution
}; };

View File

@ -1,16 +1,22 @@
const optionService = require('./options'); import optionService = require('./options');
const appInfo = require('./app_info'); import appInfo = require('./app_info');
const utils = require('./utils'); import utils = require('./utils');
const log = require('./log'); import log = require('./log');
const dateUtils = require('./date_utils'); import dateUtils = require('./date_utils');
const keyboardActions = require('./keyboard_actions'); import keyboardActions = require('./keyboard_actions');
import { KeyboardShortcutWithRequiredActionName } from './keyboard_actions_interface';
function initDocumentOptions() { function initDocumentOptions() {
optionService.createOption('documentId', utils.randomSecureToken(16), false); optionService.createOption('documentId', utils.randomSecureToken(16), false);
optionService.createOption('documentSecret', utils.randomSecureToken(16), false); optionService.createOption('documentSecret', utils.randomSecureToken(16), false);
} }
function initNotSyncedOptions(initialized, opts = {}) { interface NotSyncedOpts {
syncServerHost?: string;
syncProxy?: string;
}
function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = {}) {
optionService.createOption('openNoteContexts', JSON.stringify([ optionService.createOption('openNoteContexts', JSON.stringify([
{ {
notePath: 'root', notePath: 'root',
@ -21,7 +27,7 @@ function initNotSyncedOptions(initialized, opts = {}) {
optionService.createOption('lastDailyBackupDate', dateUtils.utcNowDateTime(), false); optionService.createOption('lastDailyBackupDate', dateUtils.utcNowDateTime(), false);
optionService.createOption('lastWeeklyBackupDate', dateUtils.utcNowDateTime(), false); optionService.createOption('lastWeeklyBackupDate', dateUtils.utcNowDateTime(), false);
optionService.createOption('lastMonthlyBackupDate', dateUtils.utcNowDateTime(), false); optionService.createOption('lastMonthlyBackupDate', dateUtils.utcNowDateTime(), false);
optionService.createOption('dbVersion', appInfo.dbVersion, false); optionService.createOption('dbVersion', appInfo.dbVersion.toString(), false);
optionService.createOption('initialized', initialized ? 'true' : 'false', false); optionService.createOption('initialized', initialized ? 'true' : 'false', false);
@ -117,8 +123,8 @@ function initStartupOptions() {
} }
function getKeyboardDefaultOptions() { function getKeyboardDefaultOptions() {
return keyboardActions.DEFAULT_KEYBOARD_ACTIONS return (keyboardActions.DEFAULT_KEYBOARD_ACTIONS
.filter(ka => !!ka.actionName) .filter(ka => !!ka.actionName) as KeyboardShortcutWithRequiredActionName[])
.map(ka => ({ .map(ka => ({
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`, name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
value: JSON.stringify(ka.defaultShortcuts), value: JSON.stringify(ka.defaultShortcuts),
@ -126,7 +132,7 @@ function getKeyboardDefaultOptions() {
})); }));
} }
module.exports = { export = {
initDocumentOptions, initDocumentOptions,
initNotSyncedOptions, initNotSyncedOptions,
initStartupOptions initStartupOptions

View File

@ -4,29 +4,11 @@ import utils = require('./utils');
import log = require('./log'); import log = require('./log');
import url = require('url'); import url = require('url');
import syncOptions = require('./sync_options'); import syncOptions = require('./sync_options');
import { ExecOpts } from './request_interface';
// this service provides abstraction over node's HTTP/HTTPS and electron net.client APIs // this service provides abstraction over node's HTTP/HTTPS and electron net.client APIs
// this allows supporting system proxy // this allows supporting system proxy
interface ExecOpts {
proxy: "noproxy" | null;
method: string;
url: string;
paging?: {
pageCount: number;
pageIndex: number;
requestId: string;
};
cookieJar?: {
header?: string;
};
auth?: {
password?: string;
},
timeout: number;
body: string;
}
interface ClientOpts { interface ClientOpts {
method: string; method: string;
url: string; url: string;
@ -230,7 +212,7 @@ function getClient(opts: ClientOpts): Client {
// it's not clear how to explicitly configure proxy (as opposed to system proxy), // it's not clear how to explicitly configure proxy (as opposed to system proxy),
// so in that case, we always use node's modules // so in that case, we always use node's modules
if (utils.isElectron() && !opts.proxy) { if (utils.isElectron() && !opts.proxy) {
return require('electron').net; return require('electron').net as Client;
} }
else { else {
const {protocol} = url.parse(opts.url); const {protocol} = url.parse(opts.url);

View File

@ -0,0 +1,20 @@
export interface CookieJar {
header?: string;
}
export interface ExecOpts {
proxy: "noproxy" | null;
method: string;
url: string;
paging?: {
pageCount: number;
pageIndex: number;
requestId: string;
};
cookieJar?: CookieJar;
auth?: {
password?: string;
},
timeout: number;
body: string;
}

View File

@ -3,7 +3,7 @@ const cls = require('./cls');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
const config = require('./config'); const config = require('./config');
const log = require('./log'); const log = require('./log');
const attributeService = require('../services/attributes.js'); const attributeService = require('../services/attributes');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const hiddenSubtreeService = require('./hidden_subtree'); const hiddenSubtreeService = require('./hidden_subtree');

View File

@ -1,12 +1,19 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
const log = require('../../log'); import log = require('../../log');
const becca = require('../../../becca/becca'); import becca = require('../../../becca/becca');
import SearchContext = require('../search_context');
class AncestorExp extends Expression { class AncestorExp extends Expression {
constructor(ancestorNoteId, ancestorDepth) {
private ancestorNoteId: string;
private ancestorDepthComparator;
ancestorDepth?: string;
constructor(ancestorNoteId: string, ancestorDepth?: string) {
super(); super();
this.ancestorNoteId = ancestorNoteId; this.ancestorNoteId = ancestorNoteId;
@ -14,7 +21,7 @@ class AncestorExp extends Expression {
this.ancestorDepthComparator = this.getComparator(ancestorDepth); this.ancestorDepthComparator = this.getComparator(ancestorDepth);
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const ancestorNote = becca.notes[this.ancestorNoteId]; const ancestorNote = becca.notes[this.ancestorNoteId];
if (!ancestorNote) { if (!ancestorNote) {
@ -44,7 +51,7 @@ class AncestorExp extends Expression {
return depthConformingNoteSet; return depthConformingNoteSet;
} }
getComparator(depthCondition) { getComparator(depthCondition?: string): ((depth: number) => boolean) | null {
if (!depthCondition) { if (!depthCondition) {
return null; return null;
} }
@ -67,4 +74,4 @@ class AncestorExp extends Expression {
} }
} }
module.exports = AncestorExp; export = AncestorExp;

View File

@ -1,11 +1,15 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import NoteSet = require('../note_set');
const TrueExp = require('./true.js'); import SearchContext = require('../search_context');
import Expression = require('./expression');
import TrueExp = require('./true');
class AndExp extends Expression { class AndExp extends Expression {
static of(subExpressions) { private subExpressions: Expression[];
subExpressions = subExpressions.filter(exp => !!exp);
static of(_subExpressions: (Expression | null | undefined)[]) {
const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[];
if (subExpressions.length === 1) { if (subExpressions.length === 1) {
return subExpressions[0]; return subExpressions[0];
@ -16,12 +20,12 @@ class AndExp extends Expression {
} }
} }
constructor(subExpressions) { constructor(subExpressions: Expression[]) {
super(); super();
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
for (const subExpression of this.subExpressions) { for (const subExpression of this.subExpressions) {
inputNoteSet = subExpression.execute(inputNoteSet, executionContext, searchContext); inputNoteSet = subExpression.execute(inputNoteSet, executionContext, searchContext);
} }
@ -30,4 +34,4 @@ class AndExp extends Expression {
} }
} }
module.exports = AndExp; export = AndExp;

View File

@ -1,11 +1,19 @@
"use strict"; "use strict";
const NoteSet = require('../note_set'); import NoteSet = require("../note_set");
const becca = require('../../../becca/becca'); import SearchContext = require("../search_context");
const Expression = require('./expression.js');
import becca = require('../../../becca/becca');
import Expression = require('./expression');
class AttributeExistsExp extends Expression { class AttributeExistsExp extends Expression {
constructor(attributeType, attributeName, prefixMatch) {
private attributeType: string;
private attributeName: string;
private isTemplateLabel: boolean;
private prefixMatch: boolean;
constructor(attributeType: string, attributeName: string, prefixMatch: boolean) {
super(); super();
this.attributeType = attributeType; this.attributeType = attributeType;
@ -15,7 +23,7 @@ class AttributeExistsExp extends Expression {
this.prefixMatch = prefixMatch; this.prefixMatch = prefixMatch;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const attrs = this.prefixMatch const attrs = this.prefixMatch
? becca.findAttributesWithPrefix(this.attributeType, this.attributeName) ? becca.findAttributesWithPrefix(this.attributeType, this.attributeName)
: becca.findAttributes(this.attributeType, this.attributeName); : becca.findAttributes(this.attributeType, this.attributeName);
@ -40,4 +48,4 @@ class AttributeExistsExp extends Expression {
} }
} }
module.exports = AttributeExistsExp; export = AttributeExistsExp;

View File

@ -1,16 +1,20 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
import SearchContext = require('../search_context');
class ChildOfExp extends Expression { class ChildOfExp extends Expression {
constructor(subExpression) {
private subExpression: Expression;
constructor(subExpression: Expression) {
super(); super();
this.subExpression = subExpression; this.subExpression = subExpression;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const subInputNoteSet = new NoteSet(); const subInputNoteSet = new NoteSet();
for (const note of inputNoteSet.notes) { for (const note of inputNoteSet.notes) {
@ -33,4 +37,4 @@ class ChildOfExp extends Expression {
} }
} }
module.exports = ChildOfExp; export = ChildOfExp;

View File

@ -1,17 +1,20 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
const becca = require('../../../becca/becca'); import becca = require('../../../becca/becca');
import SearchContext = require('../search_context');
class DescendantOfExp extends Expression { class DescendantOfExp extends Expression {
constructor(subExpression) { private subExpression: Expression;
constructor(subExpression: Expression) {
super(); super();
this.subExpression = subExpression; this.subExpression = subExpression;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const subInputNoteSet = new NoteSet(Object.values(becca.notes)); const subInputNoteSet = new NoteSet(Object.values(becca.notes));
const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext, searchContext); const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext, searchContext);
@ -25,4 +28,4 @@ class DescendantOfExp extends Expression {
} }
} }
module.exports = DescendantOfExp; export = DescendantOfExp;

View File

@ -1,17 +0,0 @@
"use strict";
class Expression {
constructor() {
this.name = this.constructor.name; // for DEBUG mode to have expression name as part of dumped JSON
}
/**
* @param {NoteSet} inputNoteSet
* @param {object} executionContext
* @param {SearchContext} searchContext
* @returns {NoteSet}
*/
execute(inputNoteSet, executionContext, searchContext) {}
}
module.exports = Expression;

View File

@ -0,0 +1,16 @@
"use strict";
import NoteSet = require("../note_set");
import SearchContext = require("../search_context");
abstract class Expression {
name: string;
constructor() {
this.name = this.constructor.name; // for DEBUG mode to have expression name as part of dumped JSON
}
abstract execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext): NoteSet;
}
export = Expression;

View File

@ -1,13 +1,14 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
import SearchContext = require('../search_context');
/** /**
* Note is hidden when all its note paths start in hidden subtree (i.e., the note is not cloned into visible tree) * Note is hidden when all its note paths start in hidden subtree (i.e., the note is not cloned into visible tree)
*/ */
class IsHiddenExp extends Expression { class IsHiddenExp extends Expression {
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
for (const note of inputNoteSet.notes) { for (const note of inputNoteSet.notes) {
@ -20,4 +21,4 @@ class IsHiddenExp extends Expression {
} }
} }
module.exports = IsHiddenExp; export = IsHiddenExp;

View File

@ -1,11 +1,19 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
const becca = require('../../../becca/becca'); import becca = require('../../../becca/becca');
import SearchContext = require('../search_context');
type Comparator = (value: string) => boolean;
class LabelComparisonExp extends Expression { class LabelComparisonExp extends Expression {
constructor(attributeType, attributeName, comparator) {
private attributeType: string;
private attributeName: string;
private comparator: Comparator;
constructor(attributeType: string, attributeName: string, comparator: Comparator) {
super(); super();
this.attributeType = attributeType; this.attributeType = attributeType;
@ -13,7 +21,7 @@ class LabelComparisonExp extends Expression {
this.comparator = comparator; this.comparator = comparator;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const attrs = becca.findAttributes(this.attributeType, this.attributeName); const attrs = becca.findAttributes(this.attributeType, this.attributeName);
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
@ -38,4 +46,4 @@ class LabelComparisonExp extends Expression {
} }
} }
module.exports = LabelComparisonExp; export = LabelComparisonExp;

View File

@ -1,19 +0,0 @@
"use strict";
const Expression = require('./expression.js');
class NotExp extends Expression {
constructor(subExpression) {
super();
this.subExpression = subExpression;
}
execute(inputNoteSet, executionContext, searchContext) {
const subNoteSet = this.subExpression.execute(inputNoteSet, executionContext, searchContext);
return inputNoteSet.minus(subNoteSet);
}
}
module.exports = NotExp;

View File

@ -0,0 +1,23 @@
"use strict";
import NoteSet = require('../note_set');
import SearchContext = require('../search_context');
import Expression = require('./expression');
class NotExp extends Expression {
private subExpression: Expression;
constructor(subExpression: Expression) {
super();
this.subExpression = subExpression;
}
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const subNoteSet = this.subExpression.execute(inputNoteSet, executionContext, searchContext);
return inputNoteSet.minus(subNoteSet);
}
}
export = NotExp;

View File

@ -1,18 +1,22 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import { NoteRow } from "../../../becca/entities/rows";
const NoteSet = require('../note_set'); import SearchContext = require("../search_context");
const log = require('../../log');
const becca = require('../../../becca/becca'); import Expression = require('./expression');
const protectedSessionService = require('../../protected_session'); import NoteSet = require('../note_set');
const striptags = require('striptags'); import log = require('../../log');
const utils = require('../../utils'); import becca = require('../../../becca/becca');
import protectedSessionService = require('../../protected_session');
import striptags = require('striptags');
import utils = require('../../utils');
import sql = require("../../sql");
const ALLOWED_OPERATORS = ['=', '!=', '*=*', '*=', '=*', '%=']; const ALLOWED_OPERATORS = ['=', '!=', '*=*', '*=', '=*', '%='];
const cachedRegexes = {}; const cachedRegexes: Record<string, RegExp> = {};
function getRegex(str) { function getRegex(str: string): RegExp {
if (!(str in cachedRegexes)) { if (!(str in cachedRegexes)) {
cachedRegexes[str] = new RegExp(str, 'ms'); // multiline, dot-all cachedRegexes[str] = new RegExp(str, 'ms'); // multiline, dot-all
} }
@ -20,8 +24,22 @@ function getRegex(str) {
return cachedRegexes[str]; return cachedRegexes[str];
} }
interface ConstructorOpts {
tokens: string[];
raw?: boolean;
flatText?: boolean;
}
type SearchRow = Pick<NoteRow, "noteId" | "type" | "mime" | "content" | "isProtected">;
class NoteContentFulltextExp extends Expression { class NoteContentFulltextExp extends Expression {
constructor(operator, {tokens, raw, flatText}) {
private operator: string;
private tokens: string[];
private raw: boolean;
private flatText: boolean;
constructor(operator: string, {tokens, raw, flatText}: ConstructorOpts) {
super(); super();
this.operator = operator; this.operator = operator;
@ -30,7 +48,7 @@ class NoteContentFulltextExp extends Expression {
this.flatText = !!flatText; this.flatText = !!flatText;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
if (!ALLOWED_OPERATORS.includes(this.operator)) { if (!ALLOWED_OPERATORS.includes(this.operator)) {
searchContext.addError(`Note content can be searched only with operators: ${ALLOWED_OPERATORS.join(", ")}, operator ${this.operator} given.`); searchContext.addError(`Note content can be searched only with operators: ${ALLOWED_OPERATORS.join(", ")}, operator ${this.operator} given.`);
@ -38,9 +56,8 @@ class NoteContentFulltextExp extends Expression {
} }
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
const sql = require('../../sql');
for (const row of sql.iterateRows(` for (const row of sql.iterateRows<SearchRow>(`
SELECT noteId, type, mime, content, isProtected SELECT noteId, type, mime, content, isProtected
FROM notes JOIN blobs USING (blobId) FROM notes JOIN blobs USING (blobId)
WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0`)) { WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0`)) {
@ -51,18 +68,18 @@ class NoteContentFulltextExp extends Expression {
return resultNoteSet; return resultNoteSet;
} }
findInText({noteId, isProtected, content, type, mime}, inputNoteSet, resultNoteSet) { findInText({noteId, isProtected, content, type, mime}: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
return; return;
} }
if (isProtected) { if (isProtected) {
if (!protectedSessionService.isProtectedSessionAvailable()) { if (!protectedSessionService.isProtectedSessionAvailable() || !content) {
return; return;
} }
try { try {
content = protectedSessionService.decryptString(content); content = protectedSessionService.decryptString(content) || undefined;
} catch (e) { } catch (e) {
log.info(`Cannot decrypt content of note ${noteId}`); log.info(`Cannot decrypt content of note ${noteId}`);
return; return;
@ -89,7 +106,7 @@ class NoteContentFulltextExp extends Expression {
} }
} else { } else {
const nonMatchingToken = this.tokens.find(token => const nonMatchingToken = this.tokens.find(token =>
!content.includes(token) && !content?.includes(token) &&
( (
// in case of default fulltext search, we should consider both title, attrs and content // 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 // so e.g. "hello world" should match when "hello" is in title and "world" in content
@ -106,7 +123,7 @@ class NoteContentFulltextExp extends Expression {
return content; return content;
} }
preprocessContent(content, type, mime) { preprocessContent(content: string, type: string, mime: string) {
content = utils.normalize(content.toString()); content = utils.normalize(content.toString());
if (type === 'text' && mime === 'text/html') { if (type === 'text' && mime === 'text/html') {
@ -120,7 +137,7 @@ class NoteContentFulltextExp extends Expression {
return content.trim(); return content.trim();
} }
stripTags(content) { stripTags(content: string) {
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 // 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) // 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 // but we don't want to insert text for typical formatting inline tags which can occur within one word
@ -138,4 +155,4 @@ class NoteContentFulltextExp extends Expression {
} }
} }
module.exports = NoteContentFulltextExp; export = NoteContentFulltextExp;

View File

@ -1,29 +1,34 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import BNote = require("../../../becca/entities/bnote");
const NoteSet = require('../note_set'); import SearchContext = require("../search_context");
const becca = require('../../../becca/becca');
const utils = require('../../utils'); import Expression = require('./expression');
import NoteSet = require('../note_set');
import becca = require('../../../becca/becca');
import utils = require('../../utils');
class NoteFlatTextExp extends Expression { class NoteFlatTextExp extends Expression {
constructor(tokens) { private tokens: string[];
constructor(tokens: string[]) {
super(); super();
this.tokens = tokens; this.tokens = tokens;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: any, searchContext: SearchContext) {
// has deps on SQL which breaks unit test so needs to be dynamically required // has deps on SQL which breaks unit test so needs to be dynamically required
const beccaService = require('../../../becca/becca_service'); const beccaService = require('../../../becca/becca_service');
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
/** /**
* @param {BNote} note * @param note
* @param {string[]} remainingTokens - tokens still needed to be found in the path towards root * @param remainingTokens - tokens still needed to be found in the path towards root
* @param {string[]} takenPath - path so far taken towards from candidate note towards the root. * @param takenPath - path so far taken towards from candidate note towards the root.
* It contains the suffix fragment of the full note path. * It contains the suffix fragment of the full note path.
*/ */
const searchPathTowardsRoot = (note, remainingTokens, takenPath) => { const searchPathTowardsRoot = (note: BNote, remainingTokens: string[], takenPath: string[]) => {
if (remainingTokens.length === 0) { if (remainingTokens.length === 0) {
// we're done, just build the result // we're done, just build the result
const resultPath = this.getNotePath(note, takenPath); const resultPath = this.getNotePath(note, takenPath);
@ -134,12 +139,7 @@ class NoteFlatTextExp extends Expression {
return resultNoteSet; return resultNoteSet;
} }
/** getNotePath(note: BNote, takenPath: string[]): string[] {
* @param {BNote} note
* @param {string[]} takenPath
* @returns {string[]}
*/
getNotePath(note, takenPath) {
if (takenPath.length === 0) { if (takenPath.length === 0) {
throw new Error("Path is not expected to be empty."); throw new Error("Path is not expected to be empty.");
} else if (takenPath.length === 1 && takenPath[0] === note.noteId) { } else if (takenPath.length === 1 && takenPath[0] === note.noteId) {
@ -147,7 +147,7 @@ class NoteFlatTextExp extends Expression {
} else { } else {
// this note is the closest to root containing the last matching token(s), thus completing the requirements // this note is the closest to root containing the last matching token(s), thus completing the requirements
// what's in this note's predecessors does not matter, thus we'll choose the best note path // what's in this note's predecessors does not matter, thus we'll choose the best note path
const topMostMatchingTokenNotePath = becca.getNote(takenPath[0]).getBestNotePath(); const topMostMatchingTokenNotePath = becca.getNote(takenPath[0])?.getBestNotePath() || [];
return [...topMostMatchingTokenNotePath, ...takenPath.slice(1)]; return [...topMostMatchingTokenNotePath, ...takenPath.slice(1)];
} }
@ -155,11 +155,8 @@ class NoteFlatTextExp extends Expression {
/** /**
* Returns noteIds which have at least one matching tokens * Returns noteIds which have at least one matching tokens
*
* @param {NoteSet} noteSet
* @returns {BNote[]}
*/ */
getCandidateNotes(noteSet) { getCandidateNotes(noteSet: NoteSet): BNote[] {
const candidateNotes = []; const candidateNotes = [];
for (const note of noteSet.notes) { for (const note of noteSet.notes) {
@ -175,4 +172,4 @@ class NoteFlatTextExp extends Expression {
} }
} }
module.exports = NoteFlatTextExp; export = NoteFlatTextExp;

View File

@ -1,11 +1,14 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
const TrueExp = require('./true.js'); import TrueExp = require('./true');
import SearchContext = require('../search_context');
class OrExp extends Expression { class OrExp extends Expression {
static of(subExpressions) { private subExpressions: Expression[];
static of(subExpressions: Expression[]) {
subExpressions = subExpressions.filter(exp => !!exp); subExpressions = subExpressions.filter(exp => !!exp);
if (subExpressions.length === 1) { if (subExpressions.length === 1) {
@ -19,13 +22,13 @@ class OrExp extends Expression {
} }
} }
constructor(subExpressions) { constructor(subExpressions: Expression[]) {
super(); super();
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
for (const subExpression of this.subExpressions) { for (const subExpression of this.subExpressions) {
@ -36,4 +39,4 @@ class OrExp extends Expression {
} }
} }
module.exports = OrExp; export = OrExp;

View File

@ -1,13 +1,31 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import BNote = require("../../../becca/entities/bnote");
const NoteSet = require('../note_set'); import NoteSet = require("../note_set");
import SearchContext = require("../search_context");
import Expression = require("./expression");
interface ValueExtractor {
extract: (note: BNote) => number | string | null;
}
interface OrderDefinition {
direction?: string;
smaller: number;
larger: number;
valueExtractor: ValueExtractor;
}
class OrderByAndLimitExp extends Expression { class OrderByAndLimitExp extends Expression {
constructor(orderDefinitions, limit) {
private orderDefinitions: OrderDefinition[];
private limit: number;
subExpression: Expression | null;
constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) {
super(); super();
this.orderDefinitions = orderDefinitions; this.orderDefinitions = orderDefinitions as OrderDefinition[];
for (const od of this.orderDefinitions) { for (const od of this.orderDefinitions) {
od.smaller = od.direction === "asc" ? -1 : 1; od.smaller = od.direction === "asc" ? -1 : 1;
@ -16,11 +34,14 @@ class OrderByAndLimitExp extends Expression {
this.limit = limit || 0; this.limit = limit || 0;
/** @type {Expression} */
this.subExpression = null; // it's expected to be set after construction this.subExpression = null; // it's expected to be set after construction
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
if (!this.subExpression) {
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) => { notes.sort((a, b) => {
@ -48,7 +69,8 @@ class OrderByAndLimitExp extends Expression {
} }
// if both are numbers, then parse them for numerical comparison // if both are numbers, then parse them for numerical comparison
if (this.isNumber(valA) && this.isNumber(valB)) { if (typeof valA === "string" && this.isNumber(valA) &&
typeof valB === "string" && this.isNumber(valB)) {
valA = parseFloat(valA); valA = parseFloat(valA);
valB = parseFloat(valB); valB = parseFloat(valB);
} }
@ -77,16 +99,16 @@ class OrderByAndLimitExp extends Expression {
return noteSet; return noteSet;
} }
isNumber(x) { isNumber(x: number | string) {
if (typeof x === 'number') { if (typeof x === 'number') {
return true; return true;
} else if (typeof x === 'string') { } else if (typeof x === 'string') {
// isNaN will return false for blank string // isNaN will return false for blank string
return x.trim() !== "" && !isNaN(x); return x.trim() !== "" && !isNaN(parseInt(x, 10));
} else { } else {
return false; return false;
} }
} }
} }
module.exports = OrderByAndLimitExp; export = OrderByAndLimitExp;

View File

@ -1,16 +1,19 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
import SearchContext = require('../search_context');
class ParentOfExp extends Expression { class ParentOfExp extends Expression {
constructor(subExpression) { private subExpression: Expression;
constructor(subExpression: Expression) {
super(); super();
this.subExpression = subExpression; this.subExpression = subExpression;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const subInputNoteSet = new NoteSet(); const subInputNoteSet = new NoteSet();
for (const note of inputNoteSet.notes) { for (const note of inputNoteSet.notes) {
@ -33,4 +36,4 @@ class ParentOfExp extends Expression {
} }
} }
module.exports = ParentOfExp; export = ParentOfExp;

View File

@ -1,14 +1,14 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
const buildComparator = require('../services/build_comparator.js'); import buildComparator = require('../services/build_comparator');
/** /**
* Search string is lower cased for case-insensitive comparison. But when retrieving properties, * Search string is lower cased for case-insensitive comparison. But when retrieving properties,
* we need the case-sensitive form, so we have this translation object. * we need the case-sensitive form, so we have this translation object.
*/ */
const PROP_MAPPING = { const PROP_MAPPING: Record<string, string> = {
"noteid": "noteId", "noteid": "noteId",
"title": "title", "title": "title",
"type": "type", "type": "type",
@ -36,12 +36,22 @@ const PROP_MAPPING = {
"revisioncount": "revisionCount" "revisioncount": "revisionCount"
}; };
interface SearchContext {
dbLoadNeeded?: boolean;
}
class PropertyComparisonExp extends Expression { class PropertyComparisonExp extends Expression {
static isProperty(name) {
private propertyName: string;
private operator: string;
private comparedValue: string;
private comparator;
static isProperty(name: string) {
return name in PROP_MAPPING; return name in PROP_MAPPING;
} }
constructor(searchContext, propertyName, operator, comparedValue) { constructor(searchContext: SearchContext, propertyName: string, operator: string, comparedValue: string) {
super(); super();
this.propertyName = PROP_MAPPING[propertyName]; this.propertyName = PROP_MAPPING[propertyName];
@ -54,11 +64,11 @@ class PropertyComparisonExp extends Expression {
} }
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const resNoteSet = new NoteSet(); const resNoteSet = new NoteSet();
for (const note of inputNoteSet.notes) { for (const note of inputNoteSet.notes) {
let value = note[this.propertyName]; 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(); value = value.toString();
@ -68,7 +78,7 @@ class PropertyComparisonExp extends Expression {
value = value.toLowerCase(); value = value.toLowerCase();
} }
if (this.comparator(value)) { if (this.comparator && this.comparator(value)) {
resNoteSet.add(note); resNoteSet.add(note);
} }
} }
@ -77,4 +87,4 @@ class PropertyComparisonExp extends Expression {
} }
} }
module.exports = PropertyComparisonExp; export = PropertyComparisonExp;

View File

@ -1,18 +1,22 @@
"use strict"; "use strict";
const Expression = require('./expression.js'); import Expression = require('./expression');
const NoteSet = require('../note_set'); import NoteSet = require('../note_set');
const becca = require('../../../becca/becca'); import becca = require('../../../becca/becca');
import SearchContext = require('../search_context');
class RelationWhereExp extends Expression { class RelationWhereExp extends Expression {
constructor(relationName, subExpression) { private relationName: string;
private subExpression: Expression;
constructor(relationName: string, subExpression: Expression) {
super(); super();
this.relationName = relationName; this.relationName = relationName;
this.subExpression = subExpression; this.subExpression = subExpression;
} }
execute(inputNoteSet, executionContext, searchContext) { execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const candidateNoteSet = new NoteSet(); const candidateNoteSet = new NoteSet();
for (const attr of becca.findAttributes('relation', this.relationName)) { for (const attr of becca.findAttributes('relation', this.relationName)) {
@ -38,4 +42,4 @@ class RelationWhereExp extends Expression {
} }
} }
module.exports = RelationWhereExp; export = RelationWhereExp;

View File

@ -1,11 +0,0 @@
"use strict";
const Expression = require('./expression.js');
class TrueExp extends Expression {
execute(inputNoteSet, executionContext, searchContext) {
return inputNoteSet;
}
}
module.exports = TrueExp;

View File

@ -0,0 +1,14 @@
"use strict";
import NoteSet = require("../note_set");
import SearchContext = require("../search_context");
import Expression = require('./expression');
class TrueExp extends Expression {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext): NoteSet {
return inputNoteSet;
}
}
export = TrueExp;

View File

@ -1,9 +1,29 @@
"use strict"; "use strict";
const hoistedNoteService = require('../hoisted_note.js'); import hoistedNoteService = require('../hoisted_note');
import { SearchParams } from './services/types';
class SearchContext { class SearchContext {
constructor(params = {}) {
fastSearch: boolean;
includeArchivedNotes: boolean;
includeHiddenNotes: boolean;
ignoreHoistedNote: boolean;
ancestorNoteId?: string;
ancestorDepth?: string;
orderBy?: string;
orderDirection?: string;
limit?: number | null;
debug?: boolean;
debugInfo: {} | null;
fuzzyAttributeSearch: boolean;
highlightedTokens: string[];
originalQuery: string;
fulltextQuery: string;
dbLoadNeeded: boolean;
private error: string | null;
constructor(params: SearchParams = {}) {
this.fastSearch = !!params.fastSearch; this.fastSearch = !!params.fastSearch;
this.includeArchivedNotes = !!params.includeArchivedNotes; this.includeArchivedNotes = !!params.includeArchivedNotes;
this.includeHiddenNotes = !!params.includeHiddenNotes; this.includeHiddenNotes = !!params.includeHiddenNotes;
@ -32,7 +52,7 @@ class SearchContext {
this.error = null; this.error = null;
} }
addError(error) { addError(error: string) {
// we record only the first error, subsequent ones are usually a consequence of the first // we record only the first error, subsequent ones are usually a consequence of the first
if (!this.error) { if (!this.error) {
this.error = error; this.error = error;
@ -48,4 +68,4 @@ class SearchContext {
} }
} }
module.exports = SearchContext; export = SearchContext;

View File

@ -1,12 +1,18 @@
"use strict"; "use strict";
const beccaService = require('../../becca/becca_service'); import beccaService = require('../../becca/becca_service');
const becca = require('../../becca/becca'); import becca = require('../../becca/becca');
class SearchResult { class SearchResult {
constructor(notePathArray) { notePathArray: string[];
score: number;
notePathTitle: string;
highlightedNotePathTitle?: string;
constructor(notePathArray: string[]) {
this.notePathArray = notePathArray; this.notePathArray = notePathArray;
this.notePathTitle = beccaService.getNoteTitleForPath(notePathArray); this.notePathTitle = beccaService.getNoteTitleForPath(notePathArray);
this.score = 0;
} }
get notePath() { get notePath() {
@ -17,7 +23,7 @@ class SearchResult {
return this.notePathArray[this.notePathArray.length - 1]; return this.notePathArray[this.notePathArray.length - 1];
} }
computeScore(fulltextQuery, tokens) { computeScore(fulltextQuery: string, tokens: string[]) {
this.score = 0; this.score = 0;
const note = becca.notes[this.noteId]; const note = becca.notes[this.noteId];
@ -42,9 +48,11 @@ class SearchResult {
} }
} }
addScoreForStrings(tokens, str, factor) { addScoreForStrings(tokens: string[], str: string, factor: number) {
const chunks = str.toLowerCase().split(" "); const chunks = str.toLowerCase().split(" ");
this.score = 0;
for (const chunk of chunks) { for (const chunk of chunks) {
for (const token of tokens) { for (const token of tokens) {
if (chunk === token) { if (chunk === token) {
@ -59,4 +67,4 @@ class SearchResult {
} }
} }
module.exports = SearchResult; export = SearchResult;

View File

@ -1,6 +1,6 @@
const cachedRegexes = {}; const cachedRegexes: Record<string, RegExp> = {};
function getRegex(str) { function getRegex(str: string) {
if (!(str in cachedRegexes)) { if (!(str in cachedRegexes)) {
cachedRegexes[str] = new RegExp(str); cachedRegexes[str] = new RegExp(str);
} }
@ -8,31 +8,36 @@ function getRegex(str) {
return cachedRegexes[str]; return cachedRegexes[str];
} }
const stringComparators = { 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 >= 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.endsWith(comparedValue)),
"=*": comparedValue => (val => val && val.startsWith(comparedValue)), "=*": comparedValue => (val => !!val && val.startsWith(comparedValue)),
"*=*": comparedValue => (val => val && val.includes(comparedValue)), "*=*": comparedValue => (val => !!val && val.includes(comparedValue)),
"%=": comparedValue => (val => val && !!getRegex(comparedValue).test(val)), "%=": comparedValue => (val => !!val && !!getRegex(comparedValue).test(val)),
}; };
const numericComparators = { 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, comparedValue) { function buildComparator(operator: string, comparedValue: string) {
comparedValue = comparedValue.toLowerCase(); comparedValue = comparedValue.toLowerCase();
if (operator in numericComparators && !isNaN(comparedValue)) { if (operator in numericComparators) {
return numericComparators[operator](parseFloat(comparedValue)); const floatValue = parseFloat(comparedValue);
if (!isNaN(floatValue)) {
return numericComparators[operator](floatValue);
}
} }
if (operator in stringComparators) { if (operator in stringComparators) {
@ -40,4 +45,4 @@ function buildComparator(operator, comparedValue) {
} }
} }
module.exports = buildComparator; export = buildComparator;

View File

@ -1,13 +1,15 @@
import { TokenData } from "./types";
/** /**
* This will create a recursive object from a list of tokens - tokens between parenthesis are grouped in a single array * This will create a recursive object from a list of tokens - tokens between parenthesis are grouped in a single array
*/ */
function handleParens(tokens) { function handleParens(tokens: (TokenData | TokenData[])[]) {
if (tokens.length === 0) { if (tokens.length === 0) {
return []; return [];
} }
while (true) { while (true) {
const leftIdx = tokens.findIndex(token => token.token === '('); const leftIdx = tokens.findIndex(token => "token" in token && token.token === '(');
if (leftIdx === -1) { if (leftIdx === -1) {
return tokens; return tokens;
@ -17,13 +19,18 @@ function handleParens(tokens) {
let parensLevel = 0 let parensLevel = 0
for (rightIdx = leftIdx; rightIdx < tokens.length; rightIdx++) { for (rightIdx = leftIdx; rightIdx < tokens.length; rightIdx++) {
if (tokens[rightIdx].token === ')') { const token = tokens[rightIdx];
if (!("token" in token)) {
continue;
}
if (token.token === ')') {
parensLevel--; parensLevel--;
if (parensLevel === 0) { if (parensLevel === 0) {
break; break;
} }
} else if (tokens[rightIdx].token === '(') { } else if (token.token === '(') {
parensLevel++; parensLevel++;
} }
} }
@ -36,8 +43,8 @@ function handleParens(tokens) {
...tokens.slice(0, leftIdx), ...tokens.slice(0, leftIdx),
handleParens(tokens.slice(leftIdx + 1, rightIdx)), handleParens(tokens.slice(leftIdx + 1, rightIdx)),
...tokens.slice(rightIdx + 1) ...tokens.slice(rightIdx + 1)
]; ] as (TokenData | TokenData[])[];
} }
} }
module.exports = handleParens; export = handleParens;

View File

@ -1,16 +1,17 @@
function lex(str) { import { TokenData } from "./types";
function lex(str: string) {
str = str.toLowerCase(); str = str.toLowerCase();
let fulltextQuery = ""; let fulltextQuery = "";
const fulltextTokens = []; const fulltextTokens: TokenData[] = [];
const expressionTokens = []; const expressionTokens: TokenData[] = [];
/** @type {boolean|string} */ let quotes: boolean | string = false; // otherwise contains used quote - ', " or `
let quotes = false; // otherwise contains used quote - ', " or `
let fulltextEnded = false; let fulltextEnded = false;
let currentWord = ''; let currentWord = '';
function isSymbolAnOperator(chr) { function isSymbolAnOperator(chr: string) {
return ['=', '*', '>', '<', '!', "-", "+", '%', ','].includes(chr); return ['=', '*', '>', '<', '!', "-", "+", '%', ','].includes(chr);
} }
@ -23,12 +24,12 @@ function lex(str) {
} }
} }
function finishWord(endIndex, createAlsoForEmptyWords = false) { function finishWord(endIndex: number, createAlsoForEmptyWords = false) {
if (currentWord === '' && !createAlsoForEmptyWords) { if (currentWord === '' && !createAlsoForEmptyWords) {
return; return;
} }
const rec = { const rec: TokenData = {
token: currentWord, token: currentWord,
inQuotes: !!quotes, inQuotes: !!quotes,
startIndex: endIndex - currentWord.length + 1, startIndex: endIndex - currentWord.length + 1,
@ -146,4 +147,4 @@ function lex(str) {
} }
} }
module.exports = lex; export = lex;

View File

@ -1,28 +1,31 @@
"use strict"; "use strict";
const dayjs = require("dayjs"); import dayjs = require("dayjs");
const AndExp = require('../expressions/and.js'); import AndExp = require('../expressions/and');
const OrExp = require('../expressions/or.js'); import OrExp = require('../expressions/or');
const NotExp = require('../expressions/not.js'); import NotExp = require('../expressions/not');
const ChildOfExp = require('../expressions/child_of.js'); import ChildOfExp = require('../expressions/child_of');
const DescendantOfExp = require('../expressions/descendant_of.js'); import DescendantOfExp = require('../expressions/descendant_of');
const ParentOfExp = require('../expressions/parent_of.js'); import ParentOfExp = require('../expressions/parent_of');
const RelationWhereExp = require('../expressions/relation_where.js'); import RelationWhereExp = require('../expressions/relation_where');
const PropertyComparisonExp = require('../expressions/property_comparison.js'); import PropertyComparisonExp = require('../expressions/property_comparison');
const AttributeExistsExp = require('../expressions/attribute_exists.js'); import AttributeExistsExp = require('../expressions/attribute_exists');
const LabelComparisonExp = require('../expressions/label_comparison.js'); import LabelComparisonExp = require('../expressions/label_comparison');
const NoteFlatTextExp = require('../expressions/note_flat_text.js'); import NoteFlatTextExp = require('../expressions/note_flat_text');
const NoteContentFulltextExp = require('../expressions/note_content_fulltext.js'); import NoteContentFulltextExp = require('../expressions/note_content_fulltext');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit.js'); import OrderByAndLimitExp = require('../expressions/order_by_and_limit');
const AncestorExp = require('../expressions/ancestor.js'); import AncestorExp = require('../expressions/ancestor');
const buildComparator = require('./build_comparator.js'); import buildComparator = require('./build_comparator');
const ValueExtractor = require('../value_extractor.js'); import ValueExtractor = require('../value_extractor');
const utils = require('../../utils'); import utils = require('../../utils');
const TrueExp = require('../expressions/true.js'); import TrueExp = require('../expressions/true');
const IsHiddenExp = require('../expressions/is_hidden.js'); import IsHiddenExp = require('../expressions/is_hidden');
import SearchContext = require("../search_context");
import { TokenData } from "./types";
import Expression = require("../expressions/expression");
function getFulltext(tokens, searchContext) { function getFulltext(_tokens: TokenData[], searchContext: SearchContext) {
tokens = tokens.map(t => utils.removeDiacritic(t.token)); const tokens: string[] = _tokens.map(t => utils.removeDiacritic(t.token));
searchContext.highlightedTokens.push(...tokens); searchContext.highlightedTokens.push(...tokens);
@ -54,7 +57,7 @@ const OPERATORS = [
"%=" "%="
]; ];
function isOperator(token) { function isOperator(token: TokenData) {
if (Array.isArray(token)) { if (Array.isArray(token)) {
return false; return false;
} }
@ -62,20 +65,20 @@ function isOperator(token) {
return OPERATORS.includes(token.token); return OPERATORS.includes(token.token);
} }
function getExpression(tokens, searchContext, level = 0) { function getExpression(tokens: TokenData[], searchContext: SearchContext, level = 0) {
if (tokens.length === 0) { if (tokens.length === 0) {
return null; return null;
} }
const expressions = []; const expressions: Expression[] = [];
let op = null; let op: string | null = null;
let i; let i: number;
function context(i) { function context(i: number) {
let {startIndex, endIndex} = tokens[i]; let {startIndex, endIndex} = tokens[i];
startIndex = Math.max(0, startIndex - 20); startIndex = Math.max(0, (startIndex || 0) - 20);
endIndex = Math.min(searchContext.originalQuery.length, endIndex + 20); endIndex = Math.min(searchContext.originalQuery.length, (endIndex || Number.MAX_SAFE_INTEGER) + 20);
return `"${startIndex !== 0 ? "..." : ""}${searchContext.originalQuery.substr(startIndex, endIndex - startIndex)}${endIndex !== searchContext.originalQuery.length ? "..." : ""}"`; return `"${startIndex !== 0 ? "..." : ""}${searchContext.originalQuery.substr(startIndex, endIndex - startIndex)}${endIndex !== searchContext.originalQuery.length ? "..." : ""}"`;
} }
@ -133,7 +136,7 @@ function getExpression(tokens, searchContext, level = 0) {
return date.format(format); return date.format(format);
} }
function parseNoteProperty() { function parseNoteProperty(): Expression | undefined | null {
if (tokens[i].token !== '.') { if (tokens[i].token !== '.') {
searchContext.addError('Expected "." to separate field path'); searchContext.addError('Expected "." to separate field path');
return; return;
@ -161,19 +164,25 @@ function getExpression(tokens, searchContext, level = 0) {
if (tokens[i].token === 'parents') { if (tokens[i].token === 'parents') {
i += 1; i += 1;
return new ChildOfExp(parseNoteProperty()); const expression = parseNoteProperty();
if (!expression) { return; }
return new ChildOfExp(expression);
} }
if (tokens[i].token === 'children') { if (tokens[i].token === 'children') {
i += 1; i += 1;
return new ParentOfExp(parseNoteProperty()); const expression = parseNoteProperty();
if (!expression) { return; }
return new ParentOfExp(expression);
} }
if (tokens[i].token === 'ancestors') { if (tokens[i].token === 'ancestors') {
i += 1; i += 1;
return new DescendantOfExp(parseNoteProperty()); const expression = parseNoteProperty();
if (!expression) { return; }
return new DescendantOfExp(expression);
} }
if (tokens[i].token === 'labels') { if (tokens[i].token === 'labels') {
@ -219,6 +228,10 @@ function getExpression(tokens, searchContext, level = 0) {
i += 2; i += 2;
const comparedValue = resolveConstantOperand(); const comparedValue = resolveConstantOperand();
if (!comparedValue) {
searchContext.addError(`Unresolved constant operand.`);
return;
}
return new PropertyComparisonExp(searchContext, propertyName, operator, comparedValue); return new PropertyComparisonExp(searchContext, propertyName, operator, comparedValue);
} }
@ -226,7 +239,7 @@ function getExpression(tokens, searchContext, level = 0) {
searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`); searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`);
} }
function parseAttribute(name) { function parseAttribute(name: string) {
const isLabel = name.startsWith('#'); const isLabel = name.startsWith('#');
name = name.substr(1); name = name.substr(1);
@ -239,10 +252,10 @@ function getExpression(tokens, searchContext, level = 0) {
const subExp = isLabel ? parseLabel(name) : parseRelation(name); const subExp = isLabel ? parseLabel(name) : parseRelation(name);
return isNegated ? new NotExp(subExp) : subExp; return subExp && isNegated ? new NotExp(subExp) : subExp;
} }
function parseLabel(labelName) { function parseLabel(labelName: string) {
searchContext.highlightedTokens.push(labelName); searchContext.highlightedTokens.push(labelName);
if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
@ -274,13 +287,15 @@ function getExpression(tokens, searchContext, level = 0) {
} }
} }
function parseRelation(relationName) { function parseRelation(relationName: string) {
searchContext.highlightedTokens.push(relationName); searchContext.highlightedTokens.push(relationName);
if (i < tokens.length - 2 && tokens[i + 1].token === '.') { if (i < tokens.length - 2 && tokens[i + 1].token === '.') {
i += 1; i += 1;
return new RelationWhereExp(relationName, parseNoteProperty()); const expression = parseNoteProperty();
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)}`); searchContext.addError(`Relation can be compared only with property, e.g. ~relation.title=hello in ${context(i)}`);
@ -293,7 +308,10 @@ function getExpression(tokens, searchContext, level = 0) {
} }
function parseOrderByAndLimit() { function parseOrderByAndLimit() {
const orderDefinitions = []; const orderDefinitions: {
valueExtractor: ValueExtractor,
direction: string
}[] = [];
let limit; let limit;
if (tokens[i].token === 'orderby') { if (tokens[i].token === 'orderby') {
@ -316,8 +334,9 @@ function getExpression(tokens, searchContext, level = 0) {
const valueExtractor = new ValueExtractor(searchContext, propertyPath); const valueExtractor = new ValueExtractor(searchContext, propertyPath);
if (valueExtractor.validate()) { const validationError = valueExtractor.validate();
searchContext.addError(valueExtractor.validate()); if (validationError) {
searchContext.addError(validationError);
} }
orderDefinitions.push({ orderDefinitions.push({
@ -348,7 +367,10 @@ function getExpression(tokens, searchContext, level = 0) {
for (i = 0; i < tokens.length; i++) { for (i = 0; i < tokens.length; i++) {
if (Array.isArray(tokens[i])) { if (Array.isArray(tokens[i])) {
expressions.push(getExpression(tokens[i], searchContext, level++)); const expression = getExpression(tokens[i] as unknown as TokenData[], searchContext, level++);
if (expression) {
expressions.push(expression);
}
continue; continue;
} }
@ -359,7 +381,10 @@ function getExpression(tokens, searchContext, level = 0) {
} }
if (token.startsWith('#') || token.startsWith('~')) { if (token.startsWith('#') || token.startsWith('~')) {
expressions.push(parseAttribute(token)); const attribute = parseAttribute(token);
if (attribute) {
expressions.push(attribute);
}
} }
else if (['orderby', 'limit'].includes(token)) { else if (['orderby', 'limit'].includes(token)) {
if (level !== 0) { if (level !== 0) {
@ -384,12 +409,17 @@ function getExpression(tokens, searchContext, level = 0) {
continue; continue;
} }
expressions.push(new NotExp(getExpression(tokens[i], searchContext, level++))); const tokenArray = tokens[i] as unknown as TokenData[];
const expression = getExpression(tokenArray, searchContext, level++);
if (!expression) { return; }
expressions.push(new NotExp(expression));
} }
else if (token === 'note') { else if (token === 'note') {
i++; i++;
expressions.push(parseNoteProperty()); const expression = parseNoteProperty();
if (!expression) { return; }
expressions.push(expression);
continue; continue;
} }
@ -416,13 +446,18 @@ function getExpression(tokens, searchContext, level = 0) {
return getAggregateExpression(); return getAggregateExpression();
} }
function parse({fulltextTokens, expressionTokens, searchContext}) { function parse({fulltextTokens, expressionTokens, searchContext}: {
let expression; fulltextTokens: TokenData[],
expressionTokens: (TokenData | TokenData[])[],
searchContext: SearchContext,
originalQuery: string
}) {
let expression: Expression | undefined | null;
try { try {
expression = getExpression(expressionTokens, searchContext); expression = getExpression(expressionTokens as TokenData[], searchContext);
} }
catch (e) { catch (e: any) {
searchContext.addError(e.message); searchContext.addError(e.message);
expression = new TrueExp(); expression = new TrueExp();
@ -441,15 +476,15 @@ function parse({fulltextTokens, expressionTokens, searchContext}) {
exp = new OrderByAndLimitExp([{ exp = new OrderByAndLimitExp([{
valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]), valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]),
direction: searchContext.orderDirection direction: searchContext.orderDirection
}], searchContext.limit); }], searchContext.limit || undefined);
exp.subExpression = filterExp; (exp as any).subExpression = filterExp;
} }
return exp; return exp;
} }
function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}) { function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}: SearchContext) {
if (ancestorNoteId && ancestorNoteId !== 'root') { if (ancestorNoteId && ancestorNoteId !== 'root') {
return new AncestorExp(ancestorNoteId, ancestorDepth); return new AncestorExp(ancestorNoteId, ancestorDepth);
} else if (!includeHiddenNotes) { } else if (!includeHiddenNotes) {
@ -459,4 +494,4 @@ function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}) {
} }
} }
module.exports = parse; export = parse;

View File

@ -1,22 +1,28 @@
"use strict"; "use strict";
const normalizeString = require("normalize-strings"); import normalizeString = require("normalize-strings");
const lex = require('./lex.js'); import lex = require('./lex');
const handleParens = require('./handle_parens.js'); import handleParens = require('./handle_parens');
const parse = require('./parse.js'); import parse = require('./parse');
const SearchResult = require('../search_result.js'); import SearchResult = require('../search_result');
const SearchContext = require('../search_context.js'); import SearchContext = require('../search_context');
const becca = require('../../../becca/becca'); import becca = require('../../../becca/becca');
const beccaService = require('../../../becca/becca_service'); import beccaService = require('../../../becca/becca_service');
const utils = require('../../utils'); import utils = require('../../utils');
const log = require('../../log'); import log = require('../../log');
const hoistedNoteService = require('../../hoisted_note.js'); import hoistedNoteService = require('../../hoisted_note');
import BNote = require("../../../becca/entities/bnote");
import BAttribute = require("../../../becca/entities/battribute");
import { SearchParams, TokenData } from "./types";
import Expression = require("../expressions/expression");
import sql = require("../../sql");
function searchFromNote(note) { function searchFromNote(note: BNote) {
let searchResultNoteIds, highlightedTokens; let searchResultNoteIds;
let highlightedTokens: string[];
const searchScript = note.getRelationValue('searchScript'); const searchScript = note.getRelationValue('searchScript');
const searchString = note.getLabelValue('searchString'); const searchString = note.getLabelValue('searchString') || "";
let error = null; let error = null;
if (searchScript) { if (searchScript) {
@ -25,12 +31,12 @@ function searchFromNote(note) {
} else { } else {
const searchContext = new SearchContext({ const searchContext = new SearchContext({
fastSearch: note.hasLabel('fastSearch'), fastSearch: note.hasLabel('fastSearch'),
ancestorNoteId: note.getRelationValue('ancestor'), ancestorNoteId: note.getRelationValue('ancestor') || undefined,
ancestorDepth: note.getLabelValue('ancestorDepth'), ancestorDepth: note.getLabelValue('ancestorDepth') || undefined,
includeArchivedNotes: note.hasLabel('includeArchivedNotes'), includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
orderBy: note.getLabelValue('orderBy'), orderBy: note.getLabelValue('orderBy') || undefined,
orderDirection: note.getLabelValue('orderDirection'), orderDirection: note.getLabelValue('orderDirection') || undefined,
limit: note.getLabelValue('limit'), limit: parseInt(note.getLabelValue('limit') || "0", 10),
debug: note.hasLabel('debug'), debug: note.hasLabel('debug'),
fuzzyAttributeSearch: false fuzzyAttributeSearch: false
}); });
@ -51,7 +57,7 @@ function searchFromNote(note) {
}; };
} }
function searchFromRelation(note, relationName) { function searchFromRelation(note: BNote, relationName: string) {
const scriptNote = note.getRelationTarget(relationName); const scriptNote = note.getRelationTarget(relationName);
if (!scriptNote) { if (!scriptNote) {
@ -90,18 +96,21 @@ function searchFromRelation(note, relationName) {
} }
function loadNeededInfoFromDatabase() { function loadNeededInfoFromDatabase() {
const sql = require('../../sql');
/** /**
* This complex structure is needed to calculate total occupied space by a note. Several object instances * This complex structure is needed to calculate total occupied space by a note. Several object instances
* (note, revisions, attachments) can point to a single blobId, and thus the blob size should count towards the total * (note, revisions, attachments) can point to a single blobId, and thus the blob size should count towards the total
* only once. * only once.
* *
* @var {Object.<string, Object.<string, int>>} - noteId => { blobId => blobSize } * noteId => { blobId => blobSize }
*/ */
const noteBlobs = {}; const noteBlobs: Record<string, Record<string, number>> = {};
const noteContentLengths = sql.getRows(` type NoteContentLengthsRow = {
noteId: string;
blobId: string;
length: number;
};
const noteContentLengths = sql.getRows<NoteContentLengthsRow>(`
SELECT SELECT
noteId, noteId,
blobId, blobId,
@ -122,7 +131,12 @@ function loadNeededInfoFromDatabase() {
noteBlobs[noteId] = { [blobId]: length }; noteBlobs[noteId] = { [blobId]: length };
} }
const attachmentContentLengths = sql.getRows(` type AttachmentContentLengthsRow = {
noteId: string;
blobId: string;
length: number;
};
const attachmentContentLengths = sql.getRows<AttachmentContentLengthsRow>(`
SELECT SELECT
ownerId AS noteId, ownerId AS noteId,
attachments.blobId, attachments.blobId,
@ -151,7 +165,13 @@ function loadNeededInfoFromDatabase() {
becca.notes[noteId].contentAndAttachmentsSize = Object.values(noteBlobs[noteId]).reduce((acc, size) => acc + size, 0); becca.notes[noteId].contentAndAttachmentsSize = Object.values(noteBlobs[noteId]).reduce((acc, size) => acc + size, 0);
} }
const revisionContentLengths = sql.getRows(` type RevisionRow = {
noteId: string;
blobId: string;
length: number;
isNoteRevision: true;
};
const revisionContentLengths = sql.getRows<RevisionRow>(`
SELECT SELECT
noteId, noteId,
revisions.blobId, revisions.blobId,
@ -187,7 +207,10 @@ function loadNeededInfoFromDatabase() {
noteBlobs[noteId][blobId] = length; noteBlobs[noteId][blobId] = length;
if (isNoteRevision) { if (isNoteRevision) {
becca.notes[noteId].revisionCount++; const noteRevision = becca.notes[noteId];
if (noteRevision && noteRevision.revisionCount) {
noteRevision.revisionCount++;
}
} }
} }
@ -196,20 +219,16 @@ function loadNeededInfoFromDatabase() {
} }
} }
/** function findResultsWithExpression(expression: Expression, searchContext: SearchContext): SearchResult[] {
* @param {Expression} expression
* @param {SearchContext} searchContext
* @returns {SearchResult[]}
*/
function findResultsWithExpression(expression, searchContext) {
if (searchContext.dbLoadNeeded) { if (searchContext.dbLoadNeeded) {
loadNeededInfoFromDatabase(); loadNeededInfoFromDatabase();
} }
const allNoteSet = becca.getAllNoteSet(); const allNoteSet = becca.getAllNoteSet();
const noteIdToNotePath: Record<string, string[]> = {};
const executionContext = { const executionContext = {
noteIdToNotePath: {} noteIdToNotePath
}; };
const noteSet = expression.execute(allNoteSet, executionContext, searchContext); const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
@ -250,16 +269,16 @@ function findResultsWithExpression(expression, searchContext) {
return searchResults; return searchResults;
} }
function parseQueryToExpression(query, searchContext) { function parseQueryToExpression(query: string, searchContext: SearchContext) {
const {fulltextQuery, fulltextTokens, expressionTokens} = lex(query); const {fulltextQuery, fulltextTokens, expressionTokens} = lex(query);
searchContext.fulltextQuery = fulltextQuery; searchContext.fulltextQuery = fulltextQuery;
let structuredExpressionTokens; let structuredExpressionTokens: (TokenData | TokenData[])[];
try { try {
structuredExpressionTokens = handleParens(expressionTokens); structuredExpressionTokens = handleParens(expressionTokens);
} }
catch (e) { catch (e: any) {
structuredExpressionTokens = []; structuredExpressionTokens = [];
searchContext.addError(e.message); searchContext.addError(e.message);
} }
@ -284,23 +303,13 @@ function parseQueryToExpression(query, searchContext) {
return expression; return expression;
} }
/** function searchNotes(query: string, params: SearchParams = {}): BNote[] {
* @param {string} query
* @param {object} params - see SearchContext
* @returns {BNote[]}
*/
function searchNotes(query, params = {}) {
const searchResults = findResultsWithQuery(query, new SearchContext(params)); 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[] {
* @param {string} query
* @param {SearchContext} searchContext
* @returns {SearchResult[]}
*/
function findResultsWithQuery(query, searchContext) {
query = query || ""; query = query || "";
searchContext.originalQuery = query; searchContext.originalQuery = query;
@ -313,18 +322,13 @@ function findResultsWithQuery(query, searchContext) {
return findResultsWithExpression(expression, searchContext); return findResultsWithExpression(expression, searchContext);
} }
/** function findFirstNoteWithQuery(query: string, searchContext: SearchContext): BNote | null {
* @param {string} query
* @param {SearchContext} searchContext
* @returns {BNote|null}
*/
function findFirstNoteWithQuery(query, searchContext) {
const searchResults = findResultsWithQuery(query, searchContext); const searchResults = findResultsWithQuery(query, searchContext);
return searchResults.length > 0 ? becca.notes[searchResults[0].noteId] : null; return searchResults.length > 0 ? becca.notes[searchResults[0].noteId] : null;
} }
function searchNotesForAutocomplete(query) { function searchNotesForAutocomplete(query: string) {
const searchContext = new SearchContext({ const searchContext = new SearchContext({
fastSearch: true, fastSearch: true,
includeArchivedNotes: false, includeArchivedNotes: false,
@ -351,7 +355,7 @@ function searchNotesForAutocomplete(query) {
}); });
} }
function highlightSearchResults(searchResults, highlightedTokens) { function highlightSearchResults(searchResults: SearchResult[], highlightedTokens: string[]) {
highlightedTokens = Array.from(new Set(highlightedTokens)); highlightedTokens = Array.from(new Set(highlightedTokens));
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks // we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
@ -387,7 +391,7 @@ function highlightSearchResults(searchResults, highlightedTokens) {
} }
} }
function wrapText(text, start, length, prefix, suffix) { function wrapText(text: string, start: number, length: number, prefix: string, suffix: string) {
return text.substring(0, start) + prefix + text.substr(start, length) + suffix + text.substring(start + length); return text.substring(0, start) + prefix + text.substr(start, length) + suffix + text.substring(start + length);
} }
@ -403,6 +407,7 @@ function highlightSearchResults(searchResults, highlightedTokens) {
let match; let match;
// Find all matches // Find all matches
if (!result.highlightedNotePathTitle) { continue; }
while ((match = tokenRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) { while ((match = tokenRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) {
result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}"); result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}");
@ -413,6 +418,7 @@ function highlightSearchResults(searchResults, highlightedTokens) {
} }
for (const result of searchResults) { for (const result of searchResults) {
if (!result.highlightedNotePathTitle) { continue; }
result.highlightedNotePathTitle = result.highlightedNotePathTitle result.highlightedNotePathTitle = result.highlightedNotePathTitle
.replace(/"/g, "<small>") .replace(/"/g, "<small>")
.replace(/'/g, "</small>") .replace(/'/g, "</small>")
@ -421,7 +427,7 @@ function highlightSearchResults(searchResults, highlightedTokens) {
} }
} }
function formatAttribute(attr) { function formatAttribute(attr: BAttribute) {
if (attr.type === 'relation') { if (attr.type === 'relation') {
return `~${utils.escapeHtml(attr.name)}=…`; return `~${utils.escapeHtml(attr.name)}=…`;
} }
@ -438,7 +444,7 @@ function formatAttribute(attr) {
} }
} }
module.exports = { export = {
searchFromNote, searchFromNote,
searchNotesForAutocomplete, searchNotesForAutocomplete,
findResultsWithQuery, findResultsWithQuery,

View File

@ -0,0 +1,20 @@
export interface TokenData {
token: string;
inQuotes?: boolean;
startIndex?: number;
endIndex?: number;
}
export interface SearchParams {
fastSearch?: boolean;
includeArchivedNotes?: boolean;
includeHiddenNotes?: boolean;
ignoreHoistedNote?: boolean;
ancestorNoteId?: string;
ancestorDepth?: string;
orderBy?: string;
orderDirection?: string;
limit?: number | null;
debug?: boolean;
fuzzyAttributeSearch?: boolean;
}

View File

@ -1,10 +1,12 @@
"use strict"; "use strict";
import BNote = require("../../becca/entities/bnote");
/** /**
* Search string is lower cased for case-insensitive comparison. But when retrieving properties, * Search string is lower cased for case-insensitive comparison. But when retrieving properties,
* we need a case-sensitive form, so we have this translation object. * we need a case-sensitive form, so we have this translation object.
*/ */
const PROP_MAPPING = { const PROP_MAPPING: Record<string, string> = {
"noteid": "noteId", "noteid": "noteId",
"title": "title", "title": "title",
"type": "type", "type": "type",
@ -32,8 +34,14 @@ const PROP_MAPPING = {
"revisioncount": "revisionCount" "revisioncount": "revisionCount"
}; };
interface SearchContext {
dbLoadNeeded: boolean;
}
class ValueExtractor { class ValueExtractor {
constructor(searchContext, propertyPath) { 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('#')) { if (this.propertyPath[0].startsWith('#')) {
@ -81,10 +89,10 @@ class ValueExtractor {
} }
} }
extract(note) { extract(note: BNote) {
let cursor = note; let cursor: BNote | null = note;
let i; let i: number = 0;
const cur = () => this.propertyPath[i]; const cur = () => this.propertyPath[i];
@ -105,8 +113,7 @@ class ValueExtractor {
i++; i++;
const attr = cursor.getAttributeCaseInsensitive('relation', cur()); const attr = cursor.getAttributeCaseInsensitive('relation', cur());
cursor = attr?.targetNote || null;
cursor = attr ? attr.targetNote : null;
} }
else if (cur() === 'parents') { else if (cur() === 'parents') {
cursor = cursor.parents[0]; cursor = cursor.parents[0];
@ -118,7 +125,7 @@ class ValueExtractor {
return Math.random().toString(); // string is expected for comparison return Math.random().toString(); // string is expected for comparison
} }
else if (cur() in PROP_MAPPING) { else if (cur() in PROP_MAPPING) {
return cursor[PROP_MAPPING[cur()]]; return (cursor as any)[PROP_MAPPING[cur()]];
} }
else { else {
// FIXME // FIXME
@ -127,4 +134,4 @@ class ValueExtractor {
} }
} }
module.exports = ValueExtractor; export = ValueExtractor;

View File

@ -1,15 +1,17 @@
"use strict"; "use strict";
const fs = require('fs'); import fs = require('fs');
const crypto = require('crypto'); import crypto = require('crypto');
const dataDir = require('./data_dir'); import dataDir = require('./data_dir');
const log = require('./log'); import log = require('./log');
const sessionSecretPath = `${dataDir.TRILIUM_DATA_DIR}/session_secret.txt`; const sessionSecretPath = `${dataDir.TRILIUM_DATA_DIR}/session_secret.txt`;
let sessionSecret; let sessionSecret;
function randomValueHex(len) { const ENCODING = "ascii";
function randomValueHex(len: number) {
return crypto.randomBytes(Math.ceil(len / 2)) return crypto.randomBytes(Math.ceil(len / 2))
.toString('hex') // convert to hexadecimal format .toString('hex') // convert to hexadecimal format
.slice(0, len).toUpperCase(); // return required number of characters .slice(0, len).toUpperCase(); // return required number of characters
@ -20,10 +22,10 @@ if (!fs.existsSync(sessionSecretPath)) {
log.info("Generated session secret"); log.info("Generated session secret");
fs.writeFileSync(sessionSecretPath, sessionSecret, 'ASCII'); fs.writeFileSync(sessionSecretPath, sessionSecret, ENCODING);
} }
else { else {
sessionSecret = fs.readFileSync(sessionSecretPath, 'ASCII'); sessionSecret = fs.readFileSync(sessionSecretPath, ENCODING);
} }
module.exports = sessionSecret; export = sessionSecret;

View File

@ -1,4 +1,4 @@
const syncService = require('./sync.js'); const syncService = require('./sync');
const log = require('./log'); const log = require('./log');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
const optionService = require('./options'); const optionService = require('./options');

View File

@ -1,12 +1,12 @@
const attributeService = require('./attributes.js'); const attributeService = require('./attributes');
const dateNoteService = require('./date_notes.js'); const dateNoteService = require('./date_notes');
const becca = require('../becca/becca'); const becca = require('../becca/becca');
const noteService = require('./notes'); const noteService = require('./notes');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const log = require('./log'); const log = require('./log');
const hoistedNoteService = require('./hoisted_note.js'); const hoistedNoteService = require('./hoisted_note');
const searchService = require('./search/services/search.js'); const searchService = require('./search/services/search');
const SearchContext = require('./search/search_context.js'); const SearchContext = require('./search/search_context');
const {LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT} = require('./hidden_subtree'); const {LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT} = require('./hidden_subtree');
function getInboxNote(date) { function getInboxNote(date) {

View File

@ -147,12 +147,12 @@ function getRawRows<T extends {} | unknown[]>(query: string, params: Params = []
return (wrap(query, s => s.raw().all(params)) as T[]) || []; return (wrap(query, s => s.raw().all(params)) as T[]) || [];
} }
function iterateRows(query: string, params: Params = []) { function iterateRows<T>(query: string, params: Params = []): IterableIterator<T> {
if (LOG_ALL_QUERIES) { if (LOG_ALL_QUERIES) {
console.log(query); console.log(query);
} }
return stmt(query).iterate(params); return stmt(query).iterate(params) as IterableIterator<T>;
} }
function getMap<K extends string | number | symbol, V>(query: string, params: Params = []) { function getMap<K extends string | number | symbol, V>(query: string, params: Params = []) {

View File

@ -84,7 +84,7 @@ async function createInitialDatabase() {
notePosition: 10 notePosition: 10
}).save(); }).save();
const optionsInitService = require('./options_init.js'); const optionsInitService = require('./options_init');
optionsInitService.initDocumentOptions(); optionsInitService.initDocumentOptions();
optionsInitService.initNotSyncedOptions(true, {}); optionsInitService.initNotSyncedOptions(true, {});
@ -132,7 +132,7 @@ function createDatabaseForSync(options: OptionRow[], syncServerHost = '', syncPr
sql.transactional(() => { sql.transactional(() => {
sql.executeScript(schema); sql.executeScript(schema);
require('./options_init.js').initNotSyncedOptions(false, { syncServerHost, syncProxy }); require('./options_init').initNotSyncedOptions(false, { syncServerHost, syncProxy });
// document options required for sync to kick off // document options required for sync to kick off
for (const opt of options) { for (const opt of options) {

View File

@ -1,27 +1,50 @@
"use strict"; "use strict";
const log = require('./log'); import log = require('./log');
const sql = require('./sql'); import sql = require('./sql');
const optionService = require('./options'); import optionService = require('./options');
const utils = require('./utils'); import utils = require('./utils');
const instanceId = require('./instance_id'); import instanceId = require('./instance_id');
const dateUtils = require('./date_utils'); import dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update.js'); import syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash.js'); import contentHashService = require('./content_hash');
const appInfo = require('./app_info'); import appInfo = require('./app_info');
const syncOptions = require('./sync_options'); import syncOptions = require('./sync_options');
const syncMutexService = require('./sync_mutex'); import syncMutexService = require('./sync_mutex');
const cls = require('./cls'); import cls = require('./cls');
const request = require('./request'); import request = require('./request');
const ws = require('./ws'); import ws = require('./ws');
const entityChangesService = require('./entity_changes'); import entityChangesService = require('./entity_changes');
const entityConstructor = require('../becca/entity_constructor'); import entityConstructor = require('../becca/entity_constructor');
const becca = require('../becca/becca'); import becca = require('../becca/becca');
import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface';
import { CookieJar, ExecOpts } from './request_interface';
let proxyToggle = true; let proxyToggle = true;
let outstandingPullCount = 0; let outstandingPullCount = 0;
interface CheckResponse {
maxEntityChangeId: number;
entityHashes: Record<string, Record<string, string>>
}
interface SyncResponse {
instanceId: string;
maxEntityChangeId: number;
}
interface ChangesResponse {
entityChanges: EntityChangeRecord[];
lastEntityChangeId: number;
outstandingPullCount: number;
}
interface SyncContext {
cookieJar: CookieJar;
instanceId?: string;
}
async function sync() { async function sync() {
try { try {
return await syncMutexService.doExclusively(async () => { return await syncMutexService.doExclusively(async () => {
@ -53,7 +76,7 @@ async function sync() {
}; };
}); });
} }
catch (e) { catch (e: any) {
// we're dynamically switching whether we're using proxy or not based on whether we encountered error with the current method // we're dynamically switching whether we're using proxy or not based on whether we encountered error with the current method
proxyToggle = !proxyToggle; proxyToggle = !proxyToggle;
@ -93,19 +116,23 @@ async function login() {
return await doLogin(); return await doLogin();
} }
async function doLogin() { async function doLogin(): Promise<SyncContext> {
const timestamp = dateUtils.utcNowDateTime(); const timestamp = dateUtils.utcNowDateTime();
const documentSecret = optionService.getOption('documentSecret'); const documentSecret = optionService.getOption('documentSecret');
const hash = utils.hmac(documentSecret, timestamp); const hash = utils.hmac(documentSecret, timestamp);
const syncContext = { cookieJar: {} }; const syncContext: SyncContext = { cookieJar: {} };
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', { const resp = await syncRequest<SyncResponse>(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp, timestamp: timestamp,
syncVersion: appInfo.syncVersion, syncVersion: appInfo.syncVersion,
hash: hash hash: hash
}); });
if (!resp) {
throw new Error("Got no response.");
}
if (resp.instanceId === instanceId) { 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.`);
} }
@ -125,7 +152,7 @@ async function doLogin() {
return syncContext; return syncContext;
} }
async function pullChanges(syncContext) { async function pullChanges(syncContext: SyncContext) {
while (true) { while (true) {
const lastSyncedPull = getLastSyncedPull(); const lastSyncedPull = getLastSyncedPull();
const logMarkerId = utils.randomString(10); // to easily pair sync events between client and server logs const logMarkerId = utils.randomString(10); // to easily pair sync events between client and server logs
@ -133,7 +160,10 @@ async function pullChanges(syncContext) {
const startDate = Date.now(); const startDate = Date.now();
const resp = await syncRequest(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; outstandingPullCount = resp.outstandingPullCount;
@ -141,7 +171,9 @@ async function pullChanges(syncContext) {
const pulledDate = Date.now(); const pulledDate = Date.now();
sql.transactional(() => { sql.transactional(() => {
syncUpdateService.updateEntities(entityChanges, syncContext.instanceId); if (syncContext.instanceId) {
syncUpdateService.updateEntities(entityChanges, syncContext.instanceId);
}
if (lastSyncedPull !== lastEntityChangeId) { if (lastSyncedPull !== lastEntityChangeId) {
setLastSyncedPull(lastEntityChangeId); setLastSyncedPull(lastEntityChangeId);
@ -156,7 +188,7 @@ async function pullChanges(syncContext) {
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`); 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) { catch (e: any) {
log.error(`Error occurred ${e.message} ${e.stack}`); log.error(`Error occurred ${e.message} ${e.stack}`);
} }
} }
@ -165,11 +197,11 @@ async function pullChanges(syncContext) {
log.info("Finished pull"); log.info("Finished pull");
} }
async function pushChanges(syncContext) { async function pushChanges(syncContext: SyncContext) {
let lastSyncedPush = getLastSyncedPush(); let lastSyncedPush: number | null | undefined = getLastSyncedPush();
while (true) { while (true) {
const entityChanges = sql.getRows('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) { if (entityChanges.length === 0) {
log.info("Nothing to push"); log.info("Nothing to push");
@ -190,7 +222,7 @@ async function pushChanges(syncContext) {
} }
}); });
if (filteredEntityChanges.length === 0) { if (filteredEntityChanges.length === 0 && lastSyncedPush) {
// there still might be more sync changes (because of batch limit), just all the current batch // there still might be more sync changes (because of batch limit), just all the current batch
// has been filtered out // has been filtered out
setLastSyncedPush(lastSyncedPush); setLastSyncedPush(lastSyncedPush);
@ -214,16 +246,22 @@ async function pushChanges(syncContext) {
lastSyncedPush = entityChangesRecords[entityChangesRecords.length - 1].entityChange.id; lastSyncedPush = entityChangesRecords[entityChangesRecords.length - 1].entityChange.id;
setLastSyncedPush(lastSyncedPush); if (lastSyncedPush) {
setLastSyncedPush(lastSyncedPush);
}
} }
} }
async function syncFinished(syncContext) { async function syncFinished(syncContext: SyncContext) {
await syncRequest(syncContext, 'POST', '/api/sync/finished'); await syncRequest(syncContext, 'POST', '/api/sync/finished');
} }
async function checkContentHash(syncContext) { async function checkContentHash(syncContext: SyncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); const resp = await syncRequest<CheckResponse>(syncContext, 'GET', '/api/sync/check');
if (!resp) {
throw new Error("Got no response.");
}
const lastSyncedPullId = getLastSyncedPull(); const lastSyncedPullId = getLastSyncedPull();
if (lastSyncedPullId < resp.maxEntityChangeId) { if (lastSyncedPullId < resp.maxEntityChangeId) {
@ -261,8 +299,12 @@ async function checkContentHash(syncContext) {
const PAGE_SIZE = 1000000; const PAGE_SIZE = 1000000;
async function syncRequest(syncContext, method, requestPath, body) { interface SyncContext {
body = body ? JSON.stringify(body) : ''; cookieJar: CookieJar
}
async function syncRequest<T extends {}>(syncContext: SyncContext, method: string, requestPath: string, _body?: {}) {
const body = _body ? JSON.stringify(_body) : '';
const timeout = syncOptions.getSyncTimeout(); const timeout = syncOptions.getSyncTimeout();
@ -272,7 +314,7 @@ async function syncRequest(syncContext, method, requestPath, body) {
const pageCount = Math.max(1, Math.ceil(body.length / PAGE_SIZE)); const pageCount = Math.max(1, Math.ceil(body.length / PAGE_SIZE));
for (let pageIndex = 0; pageIndex < pageCount; pageIndex++) { for (let pageIndex = 0; pageIndex < pageCount; pageIndex++) {
const opts = { const opts: ExecOpts = {
method, method,
url: syncOptions.getSyncServerHost() + requestPath, url: syncOptions.getSyncServerHost() + requestPath,
cookieJar: syncContext.cookieJar, cookieJar: syncContext.cookieJar,
@ -286,13 +328,13 @@ async function syncRequest(syncContext, method, requestPath, body) {
proxy: proxyToggle ? syncOptions.getSyncProxy() : null proxy: proxyToggle ? syncOptions.getSyncProxy() : null
}; };
response = await utils.timeLimit(request.exec(opts), timeout); response = await utils.timeLimit(request.exec(opts), timeout) as T;
} }
return response; return response;
} }
function getEntityChangeRow(entityChange) { function getEntityChangeRow(entityChange: EntityChange) {
const {entityName, entityId} = entityChange; const {entityName, entityId} = entityChange;
if (entityName === 'note_reordering') { if (entityName === 'note_reordering') {
@ -305,7 +347,7 @@ function getEntityChangeRow(entityChange) {
throw new Error(`Unknown entity for entity change ${JSON.stringify(entityChange)}`); throw new Error(`Unknown entity for entity change ${JSON.stringify(entityChange)}`);
} }
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); const entityRow = sql.getRow<EntityRow>(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (!entityRow) { if (!entityRow) {
log.error(`Cannot find entity for entity change ${JSON.stringify(entityChange)}`); log.error(`Cannot find entity for entity change ${JSON.stringify(entityChange)}`);
@ -317,15 +359,17 @@ function getEntityChangeRow(entityChange) {
entityRow.content = Buffer.from(entityRow.content, 'utf-8'); entityRow.content = Buffer.from(entityRow.content, 'utf-8');
} }
entityRow.content = entityRow.content.toString("base64"); if (entityRow.content) {
entityRow.content = entityRow.content.toString("base64");
}
} }
return entityRow; return entityRow;
} }
} }
function getEntityChangeRecords(entityChanges) { function getEntityChangeRecords(entityChanges: EntityChange[]) {
const records = []; const records: EntityChangeRecord[] = [];
let length = 0; let length = 0;
for (const entityChange of entityChanges) { for (const entityChange of entityChanges) {
@ -340,7 +384,7 @@ function getEntityChangeRecords(entityChanges) {
continue; continue;
} }
const record = { entityChange, entity }; const record: EntityChangeRecord = { entityChange, entity };
records.push(record); records.push(record);
@ -359,7 +403,7 @@ function getLastSyncedPull() {
return parseInt(optionService.getOption('lastSyncedPull')); return parseInt(optionService.getOption('lastSyncedPull'));
} }
function setLastSyncedPull(entityChangeId) { 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
@ -378,7 +422,7 @@ function getLastSyncedPush() {
return lastSyncedPush; return lastSyncedPush;
} }
function setLastSyncedPush(entityChangeId) { function setLastSyncedPush(entityChangeId: number) {
ws.setLastSyncedPush(entityChangeId); ws.setLastSyncedPush(entityChangeId);
const lastSyncedPushOption = becca.getOption('lastSyncedPush'); const lastSyncedPushOption = becca.getOption('lastSyncedPush');
@ -409,7 +453,7 @@ require('../becca/becca_loader').beccaLoaded.then(() => {
getLastSyncedPush(); getLastSyncedPush();
}); });
module.exports = { export = {
sync, sync,
login, login,
getEntityChangeRecords, getEntityChangeRecords,

View File

@ -1,11 +1,18 @@
const sql = require('./sql'); import sql = require('./sql');
const log = require('./log'); import log = require('./log');
const entityChangesService = require('./entity_changes'); import entityChangesService = require('./entity_changes');
const eventService = require('./events'); import eventService = require('./events');
const entityConstructor = require('../becca/entity_constructor'); import entityConstructor = require('../becca/entity_constructor');
const ws = require('./ws'); import ws = require('./ws');
import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface';
function updateEntities(entityChanges, instanceId) { interface UpdateContext {
alreadyErased: number;
erased: number;
updated: Record<string, string[]>
}
function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string) {
if (entityChanges.length === 0) { if (entityChanges.length === 0) {
return; return;
} }
@ -34,13 +41,15 @@ function updateEntities(entityChanges, instanceId) {
atLeastOnePullApplied = true; atLeastOnePullApplied = true;
} }
updateEntity(entityChange, entity, instanceId, updateContext); if (entity) {
updateEntity(entityChange, entity, instanceId, updateContext);
}
} }
logUpdateContext(updateContext); logUpdateContext(updateContext);
} }
function updateEntity(remoteEC, remoteEntityRow, instanceId, updateContext) { function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string, updateContext: UpdateContext) {
if (!remoteEntityRow && remoteEC.entityName === 'options') { if (!remoteEntityRow && remoteEC.entityName === 'options') {
return; // can be undefined for options with isSynced=false return; // can be undefined for options with isSynced=false
} }
@ -65,8 +74,12 @@ function updateEntity(remoteEC, remoteEntityRow, instanceId, updateContext) {
} }
} }
function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext) { function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string, updateContext: UpdateContext) {
const localEC = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]); const localEC = sql.getRow<EntityChange>(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
if (!localEC.utcDateChanged || !remoteEC.utcDateChanged) {
throw new Error("Missing date changed.");
}
if (!localEC || localEC.utcDateChanged <= remoteEC.utcDateChanged) { if (!localEC || localEC.utcDateChanged <= remoteEC.utcDateChanged) {
if (remoteEC.isErased) { if (remoteEC.isErased) {
@ -110,28 +123,30 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
return false; return false;
} }
function preProcessContent(remoteEC, remoteEntityRow) { 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 // 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 // "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) // is string note or note (syncs can arrive out of order)
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64'); if (typeof remoteEntityRow.content === "string") {
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
if (remoteEntityRow.content.byteLength === 0) { 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 // there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
// (possibly not a problem anymore with the newer better-sqlite3) // (possibly not a problem anymore with the newer better-sqlite3)
remoteEntityRow.content = ""; remoteEntityRow.content = "";
}
} }
} }
} }
function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) { function updateNoteReordering(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string) {
if (!remoteEntityRow) { if (!remoteEntityRow) {
throw new Error(`Empty note_reordering body for: ${JSON.stringify(remoteEC)}`); throw new Error(`Empty note_reordering body for: ${JSON.stringify(remoteEC)}`);
} }
for (const key in remoteEntityRow) { for (const key in remoteEntityRow) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remoteEntityRow[key], key]); sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remoteEntityRow[key as keyof EntityRow], key]);
} }
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId); entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
@ -139,7 +154,7 @@ function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) {
return true; return true;
} }
function eraseEntity(entityChange) { function eraseEntity(entityChange: EntityChange) {
const {entityName, entityId} = entityChange; const {entityName, entityId} = entityChange;
const entityNames = [ const entityNames = [
@ -161,7 +176,7 @@ function eraseEntity(entityChange) {
sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]); sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]);
} }
function logUpdateContext(updateContext) { function logUpdateContext(updateContext: UpdateContext) {
const message = JSON.stringify(updateContext) const message = JSON.stringify(updateContext)
.replaceAll('"', '') .replaceAll('"', '')
.replaceAll(":", ": ") .replaceAll(":", ": ")
@ -170,6 +185,6 @@ function logUpdateContext(updateContext) {
log.info(message.substr(1, message.length - 2)); log.info(message.substr(1, message.length - 2));
} }
module.exports = { export = {
updateEntities updateEntities
}; };

View File

@ -1,13 +1,9 @@
const { Menu, Tray } = require('electron'); import { Menu, Tray } from 'electron';
const path = require('path'); import path = require('path');
const windowService = require('./window.js'); import windowService = require('./window');
const optionService = require('./options'); import optionService = require('./options');
const UPDATE_TRAY_EVENTS = [ let tray: Tray;
'minimize', 'maximize', 'show', 'hide'
]
let tray = null;
// `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window // `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window
// is minimized // is minimized
let isVisible = true; let isVisible = true;
@ -37,22 +33,25 @@ const getIconPath = () => {
} }
const registerVisibilityListener = () => { const registerVisibilityListener = () => {
const mainWindow = windowService.getMainWindow(); const mainWindow = windowService.getMainWindow();
if (!mainWindow) { return; }
// They need to be registered before the tray updater is registered // They need to be registered before the tray updater is registered
mainWindow.on('show', () => { mainWindow.on('show', () => {
isVisible = true; isVisible = true;
updateTrayMenu();
}); });
mainWindow.on('hide', () => { mainWindow.on('hide', () => {
isVisible = false; isVisible = false;
updateTrayMenu();
}); });
UPDATE_TRAY_EVENTS.forEach(eventName => { mainWindow.on("minimize", updateTrayMenu);
mainWindow.on(eventName, updateTrayMenu) mainWindow.on("maximize", updateTrayMenu);
});
} }
const updateTrayMenu = () => { const updateTrayMenu = () => {
const mainWindow = windowService.getMainWindow(); const mainWindow = windowService.getMainWindow();
if (!mainWindow) { return; }
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
@ -83,6 +82,7 @@ const updateTrayMenu = () => {
} }
const changeVisibility = () => { const changeVisibility = () => {
const window = windowService.getMainWindow(); const window = windowService.getMainWindow();
if (!window) { return; }
if (isVisible) { if (isVisible) {
window.hide(); window.hide();
@ -106,6 +106,6 @@ function createTray() {
registerVisibilityListener(); registerVisibilityListener();
} }
module.exports = { export = {
createTray createTray
} }

View File

@ -1,12 +1,13 @@
"use strict"; "use strict";
const sql = require('./sql'); import sql = require('./sql');
const log = require('./log'); import log = require('./log');
const BBranch = require('../becca/entities/bbranch'); import BBranch = require('../becca/entities/bbranch');
const entityChangesService = require('./entity_changes'); import entityChangesService = require('./entity_changes');
const becca = require('../becca/becca'); import becca = require('../becca/becca');
import BNote = require('../becca/entities/bnote');
function validateParentChild(parentNoteId, childNoteId, branchId = null) { function validateParentChild(parentNoteId: string, childNoteId: string, branchId: string | null = null) {
if (['root', '_hidden', '_share', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(childNoteId)) { if (['root', '_hidden', '_share', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(childNoteId)) {
return { branch: null, success: false, message: `Cannot change this note's location.` }; return { branch: null, success: false, message: `Cannot change this note's location.` };
} }
@ -25,7 +26,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
return { return {
branch: existingBranch, branch: existingBranch,
success: false, success: false,
message: `Note "${childNote.title}" note already exists in the "${parentNote.title}".` message: `Note "${childNote?.title}" note already exists in the "${parentNote?.title}".`
}; };
} }
@ -37,7 +38,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
}; };
} }
if (parentNoteId !== '_lbBookmarks' && becca.getNote(parentNoteId).type === 'launcher') { if (parentNoteId !== '_lbBookmarks' && becca.getNote(parentNoteId)?.type === 'launcher') {
return { return {
branch: null, branch: null,
success: false, success: false,
@ -51,7 +52,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
/** /**
* Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases. * Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases.
*/ */
function wouldAddingBranchCreateCycle(parentNoteId, childNoteId) { function wouldAddingBranchCreateCycle(parentNoteId: string, childNoteId: string) {
if (parentNoteId === childNoteId) { if (parentNoteId === childNoteId) {
return true; return true;
} }
@ -70,20 +71,22 @@ function wouldAddingBranchCreateCycle(parentNoteId, childNoteId) {
return parentAncestorNoteIds.some(parentAncestorNoteId => childSubtreeNoteIds.has(parentAncestorNoteId)); return parentAncestorNoteIds.some(parentAncestorNoteId => childSubtreeNoteIds.has(parentAncestorNoteId));
} }
function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, foldersFirst = false, sortNatural = false, sortLocale) { function sortNotes(parentNoteId: string, customSortBy: string = 'title', reverse = false, foldersFirst = false, sortNatural = false, _sortLocale?: string | null) {
if (!customSortBy) { if (!customSortBy) {
customSortBy = 'title'; customSortBy = 'title';
} }
if (!sortLocale) { // sortLocale can not be empty string or null value, default value must be set to undefined.
// sortLocale can not be empty string or null value, default value must be set to undefined. const sortLocale = (_sortLocale || undefined);
sortLocale = undefined;
}
sql.transactional(() => { sql.transactional(() => {
const notes = becca.getNote(parentNoteId).getChildNotes(); const note = becca.getNote(parentNoteId);
if (!note) {
throw new Error("Unable to find note");
}
const normalize = obj => (obj && typeof obj === 'string') ? obj.toLowerCase() : obj; const notes = note.getChildNotes();
const normalize = (obj: any) => (obj && typeof obj === 'string') ? obj.toLowerCase() : obj;
notes.sort((a, b) => { notes.sort((a, b) => {
if (foldersFirst) { if (foldersFirst) {
@ -96,7 +99,7 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
} }
} }
function fetchValue(note, key) { function fetchValue(note: BNote, key: string) {
let rawValue; let rawValue;
if (key === 'title') { if (key === 'title') {
@ -105,14 +108,14 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
rawValue = prefix ? `${prefix} - ${note.title}` : note.title; rawValue = prefix ? `${prefix} - ${note.title}` : note.title;
} else { } else {
rawValue = ['dateCreated', 'dateModified'].includes(key) rawValue = ['dateCreated', 'dateModified'].includes(key)
? note[key] ? (note as any)[key]
: note.getLabelValue(key); : note.getLabelValue(key);
} }
return normalize(rawValue); return normalize(rawValue);
} }
function compare(a, b) { function compare(a: string, b: string) {
if (!sortNatural) { if (!sortNatural) {
// alphabetical sort // alphabetical sort
return b === null || b === undefined || a < b ? -1 : 1; return b === null || b === undefined || a < b ? -1 : 1;
@ -160,6 +163,7 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
for (const note of notes) { for (const note of notes) {
const branch = note.getParentBranches().find(b => b.parentNoteId === parentNoteId); const branch = note.getParentBranches().find(b => b.parentNoteId === parentNoteId);
if (!branch) { continue; }
if (branch.noteId === '_hidden') { if (branch.noteId === '_hidden') {
position = 999_999_999; position = 999_999_999;
@ -182,9 +186,8 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
}); });
} }
function sortNotesIfNeeded(parentNoteId) { function sortNotesIfNeeded(parentNoteId: string) {
const parentNote = becca.getNote(parentNoteId); const parentNote = becca.getNote(parentNoteId);
if (!parentNote) { if (!parentNote) {
return; return;
} }
@ -206,7 +209,7 @@ function sortNotesIfNeeded(parentNoteId) {
/** /**
* @deprecated this will be removed in the future * @deprecated this will be removed in the future
*/ */
function setNoteToParent(noteId, prefix, parentNoteId) { function setNoteToParent(noteId: string, prefix: string, parentNoteId: string) {
const parentNote = becca.getNote(parentNoteId); const parentNote = becca.getNote(parentNoteId);
if (parentNoteId && !parentNote) { if (parentNoteId && !parentNote) {
@ -215,7 +218,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
} }
// case where there might be more such branches is ignored. It's expected there should be just one // case where there might be more such branches is ignored. It's expected there should be just one
const branchId = sql.getValue("SELECT branchId FROM branches WHERE isDeleted = 0 AND noteId = ? AND prefix = ?", [noteId, prefix]); const branchId = sql.getValue<string>("SELECT branchId FROM branches WHERE isDeleted = 0 AND noteId = ? AND prefix = ?", [noteId, prefix]);
const branch = becca.getBranch(branchId); const branch = becca.getBranch(branchId);
if (branch) { if (branch) {
@ -233,12 +236,15 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
} }
else if (parentNoteId) { else if (parentNoteId) {
const note = becca.getNote(noteId); const note = becca.getNote(noteId);
if (!note) {
throw new Error(`Cannot find note '${noteId}.`);
}
if (note.isDeleted) { if (note.isDeleted) {
throw new Error(`Cannot create a branch for '${noteId}' which is deleted.`); throw new Error(`Cannot create a branch for '${noteId}' which is deleted.`);
} }
const branchId = sql.getValue('SELECT branchId FROM branches WHERE isDeleted = 0 AND noteId = ? AND parentNoteId = ?', [noteId, parentNoteId]); const branchId = sql.getValue<string>('SELECT branchId FROM branches WHERE isDeleted = 0 AND noteId = ? AND parentNoteId = ?', [noteId, parentNoteId]);
const branch = becca.getBranch(branchId); const branch = becca.getBranch(branchId);
if (branch) { if (branch) {
@ -255,7 +261,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
} }
} }
module.exports = { export = {
validateParentChild, validateParentChild,
sortNotes, sortNotes,
sortNotesIfNeeded, sortNotesIfNeeded,

View File

@ -238,7 +238,7 @@ function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, n
} }
} }
function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage: string): Promise<T> { function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> {
if (!promise || !promise.then) { // it's not actually a promise if (!promise || !promise.then) { // it's not actually a promise
return promise; return promise;
} }

View File

@ -1,21 +1,20 @@
const path = require('path'); import path = require('path');
const url = require("url"); import url = require("url");
const port = require('./port'); import port = require('./port');
const optionService = require('./options'); import optionService = require('./options');
const env = require('./env'); import env = require('./env');
const log = require('./log'); import log = require('./log');
const sqlInit = require('./sql_init'); import sqlInit = require('./sql_init');
const cls = require('./cls'); import cls = require('./cls');
const keyboardActionsService = require('./keyboard_actions'); import keyboardActionsService = require('./keyboard_actions');
const { ipcMain } = require('electron'); import remoteMain = require("@electron/remote/main")
import { App, BrowserWindow, WebContents, ipcMain } from 'electron';
// Prevent the window being garbage collected // Prevent the window being garbage collected
/** @type {Electron.BrowserWindow} */ let mainWindow: BrowserWindow | null;
let mainWindow; let setupWindow: BrowserWindow | null;
/** @type {Electron.BrowserWindow} */
let setupWindow;
async function createExtraWindow(extraWindowHash) { async function createExtraWindow(extraWindowHash: string) {
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled'); const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
const { BrowserWindow } = require('electron'); const { BrowserWindow } = require('electron');
@ -25,7 +24,6 @@ async function createExtraWindow(extraWindowHash) {
height: 800, height: 800,
title: 'Trilium Notes', title: 'Trilium Notes',
webPreferences: { webPreferences: {
enableRemoteModule: true,
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
spellcheck: spellcheckEnabled spellcheck: spellcheckEnabled
@ -44,7 +42,7 @@ ipcMain.on('create-extra-window', (event, arg) => {
createExtraWindow(arg.extraWindowHash); createExtraWindow(arg.extraWindowHash);
}); });
async function createMainWindow(app) { async function createMainWindow(app: App) {
const windowStateKeeper = require('electron-window-state'); // should not be statically imported const windowStateKeeper = require('electron-window-state'); // should not be statically imported
const mainWindowState = windowStateKeeper({ const mainWindowState = windowStateKeeper({
@ -64,7 +62,6 @@ async function createMainWindow(app) {
height: mainWindowState.height, height: mainWindowState.height,
title: 'Trilium Notes', title: 'Trilium Notes',
webPreferences: { webPreferences: {
enableRemoteModule: true,
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
spellcheck: spellcheckEnabled, spellcheck: spellcheckEnabled,
@ -95,8 +92,12 @@ async function createMainWindow(app) {
}); });
} }
function configureWebContents(webContents, spellcheckEnabled) { function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
require("@electron/remote/main").enable(webContents); if (!mainWindow) {
return;
}
remoteMain.enable(webContents);
mainWindow.webContents.setWindowOpenHandler((details) => { mainWindow.webContents.setWindowOpenHandler((details) => {
require("electron").shell.openExternal(details.url); require("electron").shell.openExternal(details.url);
@ -108,8 +109,7 @@ function configureWebContents(webContents, spellcheckEnabled) {
const parsedUrl = url.parse(targetUrl); const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages // we still need to allow internal redirects from setup and migration pages
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/' && parsedUrl.path !== '/?')) { if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname || "") || (parsedUrl.path && parsedUrl.path !== '/' && parsedUrl.path !== '/?')) {
ev.preventDefault(); ev.preventDefault();
} }
}); });
@ -168,6 +168,10 @@ async function registerGlobalShortcuts() {
const translatedShortcut = shortcut.substr(7); const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(() => { const result = globalShortcut.register(translatedShortcut, cls.wrap(() => {
if (!mainWindow) {
return;
}
// window may be hidden / not in focus // window may be hidden / not in focus
mainWindow.focus(); mainWindow.focus();
@ -189,8 +193,7 @@ function getMainWindow() {
return mainWindow; return mainWindow;
} }
export = {
module.exports = {
createMainWindow, createMainWindow,
createSetupWindow, createSetupWindow,
closeSetupWindow, closeSetupWindow,

View File

@ -9,8 +9,8 @@ const shareRoot = require('./share_root.js');
const contentRenderer = require('./content_renderer.js'); const contentRenderer = require('./content_renderer.js');
const assetPath = require('../services/asset_path'); const assetPath = require('../services/asset_path');
const appPath = require('../services/app_path'); const appPath = require('../services/app_path');
const searchService = require('../services/search/services/search.js'); const searchService = require('../services/search/services/search');
const SearchContext = require('../services/search/search_context.js'); const SearchContext = require('../services/search/search_context');
const log = require('../services/log'); const log = require('../services/log');
/** /**

View File

@ -6,9 +6,9 @@
require('../becca/entity_constructor'); require('../becca/entity_constructor');
const sqlInit = require('../services/sql_init'); const sqlInit = require('../services/sql_init');
const noteService = require('../services/notes'); const noteService = require('../services/notes');
const attributeService = require('../services/attributes.js'); const attributeService = require('../services/attributes');
const cls = require('../services/cls'); const cls = require('../services/cls');
const cloningService = require('../services/cloning.js'); const cloningService = require('../services/cloning');
const loremIpsum = require('lorem-ipsum').loremIpsum; const loremIpsum = require('lorem-ipsum').loremIpsum;
const noteCount = parseInt(process.argv[2]); const noteCount = parseInt(process.argv[2]);

5
src/types.d.ts vendored
View File

@ -12,3 +12,8 @@ declare module 'html2plaintext' {
function html2plaintext(htmlText: string): string; function html2plaintext(htmlText: string): string;
export = html2plaintext; export = html2plaintext;
} }
declare module 'normalize-strings' {
function normalizeString(string: string): string;
export = normalizeString;
}

View File

@ -45,7 +45,7 @@ function startTrilium() {
* instead of the new one. This is complicated by the fact that it is possible to run multiple instances of Trilium * instead of the new one. This is complicated by the fact that it is possible to run multiple instances of Trilium
* if port and data dir are configured separately. This complication is the source of the following weird usage. * if port and data dir are configured separately. This complication is the source of the following weird usage.
* *
* The line below makes sure that the "second-instance" (process in window.js) is fired. Normally it returns a boolean * The line below makes sure that the "second-instance" (process in window.ts) is fired. Normally it returns a boolean
* indicating whether another instance is running or not, but we ignore that and kill the app only based on the port conflict. * indicating whether another instance is running or not, but we ignore that and kill the app only based on the port conflict.
* *
* A bit weird is that "second-instance" is triggered also on the valid usecases (different port/data dir) and * A bit weird is that "second-instance" is triggered also on the valid usecases (different port/data dir) and
@ -126,26 +126,26 @@ function startHttpServer() {
} }
httpServer.on('error', error => { httpServer.on('error', error => {
if (!listenOnTcp || error.syscall !== 'listen') { if (!listenOnTcp || error.syscall !== 'listen') {
throw error; throw error;
}
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(`Port ${port} requires elevated privileges. It's recommended to use port above 1024.`);
process.exit(1);
break;
case 'EADDRINUSE':
console.error(`Port ${port} is already in use. Most likely, another Trilium process is already running. You might try to find it, kill it, and try again.`);
process.exit(1);
break;
default:
throw error;
}
} }
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(`Port ${port} requires elevated privileges. It's recommended to use port above 1024.`);
process.exit(1);
break;
case 'EADDRINUSE':
console.error(`Port ${port} is already in use. Most likely, another Trilium process is already running. You might try to find it, kill it, and try again.`);
process.exit(1);
break;
default:
throw error;
}
}
) )
httpServer.on('listening', () => { httpServer.on('listening', () => {