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 \
--found=filelist.txt \
--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);
}
const port = require('./src/services/port.ts');
const host = require('./src/services/host.js');
const port = require('./src/services/port');
const host = require('./src/services/host');
const options = { timeout: 2000 };

View File

@ -3,8 +3,8 @@
const {app, globalShortcut, BrowserWindow} = require('electron');
const sqlInit = require('./src/services/sql_init');
const appIconService = require('./src/services/app_icon.js');
const windowService = require('./src/services/window.js');
const tray = require('./src/services/tray.js');
const windowService = require('./src/services/window');
const tray = require('./src/services/tray');
// Adds debug features like hotkeys for triggering dev tools and reload
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", () => {
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", () => {
it("handles parens", () => {

View File

@ -1,5 +1,5 @@
const SearchContext = require('../../src/services/search/search_context.js');
const parse = require('../../src/services/search/services/parse.js');
const SearchContext = require('../../src/services/search/search_context');
const parse = require('../../src/services/search/services/parse');
function tokens(toks, cur = 0) {
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 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 becca = require('../../src/becca/becca.js');
const {NoteBuilder, findNoteByTitle, note} = require('./becca_mocking.js');

View File

@ -1,7 +1,7 @@
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 SearchContext = require('../../src/services/search/search_context.js');
const SearchContext = require('../../src/services/search/search_context');
const dsc = new SearchContext();

View File

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

View File

@ -20,7 +20,7 @@ const beccaLoaded = new Promise<void>((res, rej) => {
cls.init(() => {
load();
require('../services/options_init.js').initStartupOptions();
require('../services/options_init').initStartupOptions();
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({
noteId: this.noteId,
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);
if (existingBranch) {
existingBranch.notePosition = notePosition;
if (notePosition) {
existingBranch.notePosition = notePosition;
}
return existingBranch;
} else {
return new BBranch({
noteId: this.noteId,
parentNoteId: parentNoteId,
notePosition: notePosition,
notePosition: notePosition || null,
prefix: this.prefix,
isExpanded: this.isExpanded
});

View File

@ -78,13 +78,13 @@ class BNote extends AbstractBeccaEntity<BNote> {
// following attributes are filled during searching in the database
/** size of the content in bytes */
private contentSize!: number | null;
contentSize!: number | null;
/** 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 */
private contentAndAttachmentsAndRevisionsSize!: number | null;
contentAndAttachmentsAndRevisionsSize!: number | null;
/** number of note revisions for this note */
private revisionCount!: number | null;
revisionCount!: number | null;
constructor(row?: Partial<NoteRow>) {
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();
value = value ? value.toLowerCase() : null;

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 mappers = require('./mappers.js');

View File

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

View File

@ -1,7 +1,7 @@
"use strict";
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 utils = require('../../services/utils');
const cls = require('../../services/cls');

View File

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

View File

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

View File

@ -1,9 +1,9 @@
"use strict";
const attributeService = require('../../services/attributes.js');
const cloneService = require('../../services/cloning.js');
const attributeService = require('../../services/attributes');
const cloneService = require('../../services/cloning');
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 imageService = require('../../services/image.js');
const appInfo = require('../../services/app_info');

View File

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

View File

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

View File

@ -2,7 +2,7 @@
const optionService = require('../../services/options');
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');
// options allowed to be updated directly in the Options dialog

View File

@ -1,9 +1,9 @@
"use strict";
const scriptService = require('../../services/script.js');
const attributeService = require('../../services/attributes.js');
const attributeService = require('../../services/attributes');
const becca = require('../../becca/becca');
const syncService = require('../../services/sync.js');
const syncService = require('../../services/sync');
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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,12 +27,12 @@ const notesApiRoute = require('./api/notes.js');
const branchesApiRoute = require('./api/branches.js');
const attachmentsApiRoute = require('./api/attachments.js');
const autocompleteApiRoute = require('./api/autocomplete.js');
const cloningApiRoute = require('./api/cloning.js');
const cloningApiRoute = require('./api/cloning');
const revisionsApiRoute = require('./api/revisions');
const recentChangesApiRoute = require('./api/recent_changes.js');
const optionsApiRoute = require('./api/options.js');
const passwordApiRoute = require('./api/password');
const syncApiRoute = require('./api/sync.js');
const syncApiRoute = require('./api/sync');
const loginApiRoute = require('./api/login.js');
const recentNotesRoute = require('./api/recent_notes.js');
const appInfoRoute = require('./api/app_info');
@ -42,11 +42,11 @@ const setupApiRoute = require('./api/setup.js');
const sqlRoute = require('./api/sql');
const databaseRoute = require('./api/database.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 senderRoute = require('./api/sender.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 specialNotesRoute = require('./api/special_notes.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 etapiAppInfoRoutes = require('../etapi/app_info');
const etapiAttachmentRoutes = require('../etapi/attachments.js');
const etapiAttributeRoutes = require('../etapi/attributes.js');
const etapiAttributeRoutes = require('../etapi/attributes');
const etapiBranchRoutes = require('../etapi/branches.js');
const etapiNoteRoutes = require('../etapi/notes.js');
const etapiSpecialNoteRoutes = require('../etapi/special_notes.js');

View File

@ -1,5 +1,5 @@
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 FileStore = require('session-file-store')(session);

View File

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

View File

@ -1,8 +1,8 @@
"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 = '';
if (attr.type === 'label') {

View File

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

View File

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

View File

@ -1,7 +1,8 @@
const treeService = require('./tree.js');
const sql = require('./sql');
import treeService = require('./tree');
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) {
return {success: true}; // no-op
}
@ -12,8 +13,8 @@ function moveBranchToNote(branchToMove, targetParentNoteId) {
return [200, validationResult];
}
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [targetParentNoteId]);
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
const maxNotePos = sql.getValue<number | null>('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [targetParentNoteId]);
const newNotePos = !maxNotePos ? 0 : maxNotePos + 10;
const newBranch = branchToMove.createClone(targetParentNoteId, newNotePos);
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);
if (!res.success) {
if (!("success" in res) || !res.success) {
return res;
}
@ -42,7 +43,7 @@ function moveBranchToBranch(branchToMove, targetParentBranch) {
return res;
}
module.exports = {
export = {
moveBranchToBranch,
moveBranchToNote
};

View File

@ -1,12 +1,30 @@
const log = require('./log');
const revisionService = require('./revisions');
const becca = require('../becca/becca');
const cloningService = require('./cloning.js');
const branchService = require('./branches.js');
const utils = require('./utils');
const eraseService = require("./erase");
import log = require('./log');
import becca = require('../becca/becca');
import cloningService = require('./cloning');
import branchService = require('./branches');
import utils = require('./utils');
import 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) => {
note.addLabel(action.labelName, action.labelValue);
},
@ -19,7 +37,10 @@ const ACTION_HANDLERS = {
note.deleteNote(deleteId);
},
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) => {
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')
.map(actionLabel => {
let action;
@ -129,7 +150,7 @@ function getActions(note) {
.filter(a => !!a);
}
function executeActions(note, searchResultNoteIds) {
function executeActions(note: BNote, searchResultNoteIds: string[]) {
const actions = getActions(note);
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)}`);
ACTION_HANDLERS[action.name](action, resultNote);
} catch (e) {
} catch (e: any) {
log.error(`ExecuteScript search action failed with ${e.message}`);
}
}
}
}
module.exports = {
export = {
executeActions
};

View File

@ -2,12 +2,12 @@
const sql = require('./sql');
const eventChangesService = require('./entity_changes');
const treeService = require('./tree.js');
const treeService = require('./tree');
const BBranch = require('../becca/entities/bbranch');
const becca = require('../becca/becca');
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)) {
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);
if (!parentBranch) {
@ -58,7 +58,7 @@ function cloneNoteToBranch(noteId, parentBranchId, prefix) {
return ret;
}
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefix: string) {
if (!(noteId in becca.notes)) {
return { branch: null, success: false, message: `Note '${noteId}' is deleted.` };
} else if (!(parentNoteId in becca.notes)) {
@ -89,7 +89,7 @@ function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
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 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) {
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)) {
return { success: false, message: `Cloning the note '${noteId}' is forbidden.` };
}
@ -175,7 +175,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
return { success: true, branchId: branch.branchId };
}
module.exports = {
export = {
cloneNoteToBranch,
cloneNoteToParentNote,
ensureNoteIsPresentInParent,

View File

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

View File

@ -1,13 +1,14 @@
"use strict";
const noteService = require('./notes');
const attributeService = require('./attributes.js');
const dateUtils = require('./date_utils');
const sql = require('./sql');
const protectedSessionService = require('./protected_session');
const searchService = require('../services/search/services/search.js');
const SearchContext = require('../services/search/search_context.js');
const hoistedNoteService = require('./hoisted_note.js');
import noteService = require('./notes');
import attributeService = require('./attributes');
import dateUtils = require('./date_utils');
import sql = require('./sql');
import protectedSessionService = require('./protected_session');
import searchService = require('../services/search/services/search');
import SearchContext = require('../services/search/search_context');
import hoistedNoteService = require('./hoisted_note');
import BNote = require('../becca/entities/bnote');
const CALENDAR_ROOT_LABEL = 'calendarRoot';
const YEAR_LABEL = 'yearNote';
@ -17,7 +18,9 @@ const DATE_LABEL = 'dateNote';
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
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({
parentNoteId: parentNote.noteId,
title: noteTitle,
@ -27,13 +30,12 @@ function createNote(parentNote, noteTitle) {
}).note;
}
/** @returns {BNote} */
function getRootCalendarNote() {
function getRootCalendarNote(): BNote {
let rootNote;
const workspaceNote = hoistedNoteService.getWorkspaceNote();
if (!workspaceNote.isRoot()) {
if (!workspaceNote || !workspaceNote.isRoot()) {
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, rootNote = null) {
if (!rootNote) {
rootNote = getRootCalendarNote();
}
function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote();
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 monthName = MONTHS[dateObj.getMonth()];
@ -102,11 +101,8 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
.replace(/{month}/g, monthName);
}
/** @returns {BNote} */
function getMonthNote(dateStr, rootNote = null) {
if (!rootNote) {
rootNote = getRootCalendarNote();
}
function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote();
const monthStr = dateStr.substr(0, 7);
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 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 */
function ordinal(dayNumber) {
function ordinal(dayNumber: number) {
const suffixes = ["th", "st", "nd", "rd"];
const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0];
return `${dayNumber}${suffix}`;
}
/** @returns {BNote} */
function getDayNote(dateStr, rootNote = null) {
if (!rootNote) {
rootNote = getRootCalendarNote();
}
function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote();
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) {
return getDayNote(dateUtils.localNowDate(), rootNote);
}
function getStartOfTheWeek(date, startOfTheWeek) {
function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) {
const day = date.getDay();
let diff;
@ -219,7 +212,11 @@ function getStartOfTheWeek(date, startOfTheWeek) {
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 dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek);
@ -229,7 +226,7 @@ function getWeekNote(dateStr, options = {}, rootNote = null) {
return getDayNote(dateStr, rootNote);
}
module.exports = {
export = {
getRootCalendarNote,
getYearNote,
getMonthNote,

View File

@ -13,3 +13,13 @@ export interface EntityChange {
changeId?: 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 scriptService = require('./script.js');
const treeService = require('./tree.js');
const treeService = require('./tree');
const noteService = require('./notes');
const becca = require('../becca/becca');
const BAttribute = require('../becca/entities/battribute');
const hiddenSubtreeService = require('./hidden_subtree');
const oneTimeTimer = require('./one_time_timer.js');
const oneTimeTimer = require('./one_time_timer');
function runAttachedRelations(note, relationName, originEntity) {
if (!note) {

View File

@ -1,5 +1,5 @@
const cls = require('./cls');
const becca = require('../becca/becca');
import cls = require('./cls');
import becca = require('../becca/becca');
function getHoistedNoteId() {
return cls.getHoistedNoteId();
@ -26,14 +26,14 @@ function isHoistedInHiddenSubtree() {
function getWorkspaceNote() {
const hoistedNote = becca.getNote(cls.getHoistedNoteId());
if (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace')) {
if (hoistedNote && (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace'))) {
return hoistedNote;
} else {
return becca.getRoot();
}
}
module.exports = {
export = {
getHoistedNoteId,
getWorkspaceNote,
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 log = require('../../services/log');
const noteService = require('../../services/notes');
const attributeService = require('../../services/attributes.js');
const attributeService = require('../../services/attributes');
const BBranch = require('../../becca/entities/bbranch');
const path = require('path');
const protectedSessionService = require('../protected_session');
const mimeService = require('./mime.js');
const treeService = require('../tree.js');
const treeService = require('../tree');
const yauzl = require("yauzl");
const htmlSanitizer = require('../html_sanitizer');
const becca = require('../../becca/becca');

View File

@ -3,19 +3,11 @@
import optionService = require('./options');
import log = require('./log');
import utils = require('./utils');
import { KeyboardShortcut } from './keyboard_actions_interface';
const isMac = process.platform === "darwin";
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,
* 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;
utcDateCreated?: string;
ignoreForbiddenParents?: boolean;
target?: "into";
}
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.
@ -6,7 +6,7 @@ const scheduledExecutions = {};
* The good thing about synchronous better-sqlite3 is that this cannot interrupt transaction. The execution will be called
* only outside of a transaction.
*/
function scheduleExecution(name, milliseconds, cb) {
function scheduleExecution(name: string, milliseconds: number, cb: () => void) {
if (name in scheduledExecutions) {
return;
}
@ -20,6 +20,6 @@ function scheduleExecution(name, milliseconds, cb) {
}, milliseconds);
}
module.exports = {
export = {
scheduleExecution
};

View File

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

View File

@ -4,29 +4,11 @@ import utils = require('./utils');
import log = require('./log');
import url = require('url');
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 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 {
method: 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),
// so in that case, we always use node's modules
if (utils.isElectron() && !opts.proxy) {
return require('electron').net;
return require('electron').net as Client;
}
else {
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 config = require('./config');
const log = require('./log');
const attributeService = require('../services/attributes.js');
const attributeService = require('../services/attributes');
const protectedSessionService = require('../services/protected_session');
const hiddenSubtreeService = require('./hidden_subtree');

View File

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

View File

@ -1,11 +1,15 @@
"use strict";
const Expression = require('./expression.js');
const TrueExp = require('./true.js');
import NoteSet = require('../note_set');
import SearchContext = require('../search_context');
import Expression = require('./expression');
import TrueExp = require('./true');
class AndExp extends Expression {
static of(subExpressions) {
subExpressions = subExpressions.filter(exp => !!exp);
private subExpressions: Expression[];
static of(_subExpressions: (Expression | null | undefined)[]) {
const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[];
if (subExpressions.length === 1) {
return subExpressions[0];
@ -16,12 +20,12 @@ class AndExp extends Expression {
}
}
constructor(subExpressions) {
constructor(subExpressions: Expression[]) {
super();
this.subExpressions = subExpressions;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
for (const subExpression of this.subExpressions) {
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";
const NoteSet = require('../note_set');
const becca = require('../../../becca/becca');
const Expression = require('./expression.js');
import NoteSet = require("../note_set");
import SearchContext = require("../search_context");
import becca = require('../../../becca/becca');
import Expression = require('./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();
this.attributeType = attributeType;
@ -15,7 +23,7 @@ class AttributeExistsExp extends Expression {
this.prefixMatch = prefixMatch;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const attrs = this.prefixMatch
? becca.findAttributesWithPrefix(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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
import Expression = require('./expression');
import NoteSet = require('../note_set');
import SearchContext = require('../search_context');
class ChildOfExp extends Expression {
constructor(subExpression) {
private subExpression: Expression;
constructor(subExpression: Expression) {
super();
this.subExpression = subExpression;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const subInputNoteSet = new NoteSet();
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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
const becca = require('../../../becca/becca');
import Expression = require('./expression');
import NoteSet = require('../note_set');
import becca = require('../../../becca/becca');
import SearchContext = require('../search_context');
class DescendantOfExp extends Expression {
constructor(subExpression) {
private subExpression: Expression;
constructor(subExpression: Expression) {
super();
this.subExpression = subExpression;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const subInputNoteSet = new NoteSet(Object.values(becca.notes));
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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
import Expression = require('./expression');
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)
*/
class IsHiddenExp extends Expression {
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const resultNoteSet = new NoteSet();
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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
const becca = require('../../../becca/becca');
import Expression = require('./expression');
import NoteSet = require('../note_set');
import becca = require('../../../becca/becca');
import SearchContext = require('../search_context');
type Comparator = (value: string) => boolean;
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();
this.attributeType = attributeType;
@ -13,7 +21,7 @@ class LabelComparisonExp extends Expression {
this.comparator = comparator;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const attrs = becca.findAttributes(this.attributeType, this.attributeName);
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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
const log = require('../../log');
const becca = require('../../../becca/becca');
const protectedSessionService = require('../../protected_session');
const striptags = require('striptags');
const utils = require('../../utils');
import { NoteRow } from "../../../becca/entities/rows";
import SearchContext = require("../search_context");
import Expression = require('./expression');
import NoteSet = require('../note_set');
import log = require('../../log');
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 cachedRegexes = {};
const cachedRegexes: Record<string, RegExp> = {};
function getRegex(str) {
function getRegex(str: string): RegExp {
if (!(str in cachedRegexes)) {
cachedRegexes[str] = new RegExp(str, 'ms'); // multiline, dot-all
}
@ -20,8 +24,22 @@ function getRegex(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 {
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();
this.operator = operator;
@ -30,7 +48,7 @@ class NoteContentFulltextExp extends Expression {
this.flatText = !!flatText;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
if (!ALLOWED_OPERATORS.includes(this.operator)) {
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 sql = require('../../sql');
for (const row of sql.iterateRows(`
for (const row of sql.iterateRows<SearchRow>(`
SELECT noteId, type, mime, content, isProtected
FROM notes JOIN blobs USING (blobId)
WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0`)) {
@ -51,18 +68,18 @@ class NoteContentFulltextExp extends Expression {
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)) {
return;
}
if (isProtected) {
if (!protectedSessionService.isProtectedSessionAvailable()) {
if (!protectedSessionService.isProtectedSessionAvailable() || !content) {
return;
}
try {
content = protectedSessionService.decryptString(content);
content = protectedSessionService.decryptString(content) || undefined;
} catch (e) {
log.info(`Cannot decrypt content of note ${noteId}`);
return;
@ -89,7 +106,7 @@ class NoteContentFulltextExp extends Expression {
}
} else {
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
// 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;
}
preprocessContent(content, type, mime) {
preprocessContent(content: string, type: string, mime: string) {
content = utils.normalize(content.toString());
if (type === 'text' && mime === 'text/html') {
@ -120,7 +137,7 @@ class NoteContentFulltextExp extends Expression {
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 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
@ -138,4 +155,4 @@ class NoteContentFulltextExp extends Expression {
}
}
module.exports = NoteContentFulltextExp;
export = NoteContentFulltextExp;

View File

@ -1,29 +1,34 @@
"use strict";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
const becca = require('../../../becca/becca');
const utils = require('../../utils');
import BNote = require("../../../becca/entities/bnote");
import SearchContext = require("../search_context");
import Expression = require('./expression');
import NoteSet = require('../note_set');
import becca = require('../../../becca/becca');
import utils = require('../../utils');
class NoteFlatTextExp extends Expression {
constructor(tokens) {
private tokens: string[];
constructor(tokens: string[]) {
super();
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
const beccaService = require('../../../becca/becca_service');
const resultNoteSet = new NoteSet();
/**
* @param {BNote} note
* @param {string[]} 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.
* It contains the suffix fragment of the full note path.
* @param note
* @param remainingTokens - tokens still needed to be found in the path towards root
* @param takenPath - path so far taken towards from candidate note towards the root.
* 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) {
// we're done, just build the result
const resultPath = this.getNotePath(note, takenPath);
@ -134,12 +139,7 @@ class NoteFlatTextExp extends Expression {
return resultNoteSet;
}
/**
* @param {BNote} note
* @param {string[]} takenPath
* @returns {string[]}
*/
getNotePath(note, takenPath) {
getNotePath(note: BNote, takenPath: string[]): string[] {
if (takenPath.length === 0) {
throw new Error("Path is not expected to be empty.");
} else if (takenPath.length === 1 && takenPath[0] === note.noteId) {
@ -147,7 +147,7 @@ class NoteFlatTextExp extends Expression {
} else {
// 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
const topMostMatchingTokenNotePath = becca.getNote(takenPath[0]).getBestNotePath();
const topMostMatchingTokenNotePath = becca.getNote(takenPath[0])?.getBestNotePath() || [];
return [...topMostMatchingTokenNotePath, ...takenPath.slice(1)];
}
@ -155,11 +155,8 @@ class NoteFlatTextExp extends Expression {
/**
* Returns noteIds which have at least one matching tokens
*
* @param {NoteSet} noteSet
* @returns {BNote[]}
*/
getCandidateNotes(noteSet) {
getCandidateNotes(noteSet: NoteSet): BNote[] {
const candidateNotes = [];
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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
const TrueExp = require('./true.js');
import Expression = require('./expression');
import NoteSet = require('../note_set');
import TrueExp = require('./true');
import SearchContext = require('../search_context');
class OrExp extends Expression {
static of(subExpressions) {
private subExpressions: Expression[];
static of(subExpressions: Expression[]) {
subExpressions = subExpressions.filter(exp => !!exp);
if (subExpressions.length === 1) {
@ -19,13 +22,13 @@ class OrExp extends Expression {
}
}
constructor(subExpressions) {
constructor(subExpressions: Expression[]) {
super();
this.subExpressions = subExpressions;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const resultNoteSet = new NoteSet();
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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
import BNote = require("../../../becca/entities/bnote");
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 {
constructor(orderDefinitions, limit) {
private orderDefinitions: OrderDefinition[];
private limit: number;
subExpression: Expression | null;
constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) {
super();
this.orderDefinitions = orderDefinitions;
this.orderDefinitions = orderDefinitions as OrderDefinition[];
for (const od of this.orderDefinitions) {
od.smaller = od.direction === "asc" ? -1 : 1;
@ -16,11 +34,14 @@ class OrderByAndLimitExp extends Expression {
this.limit = limit || 0;
/** @type {Expression} */
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);
notes.sort((a, b) => {
@ -48,7 +69,8 @@ class OrderByAndLimitExp extends Expression {
}
// 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);
valB = parseFloat(valB);
}
@ -77,16 +99,16 @@ class OrderByAndLimitExp extends Expression {
return noteSet;
}
isNumber(x) {
isNumber(x: number | string) {
if (typeof x === 'number') {
return true;
} else if (typeof x === 'string') {
// isNaN will return false for blank string
return x.trim() !== "" && !isNaN(x);
return x.trim() !== "" && !isNaN(parseInt(x, 10));
} else {
return false;
}
}
}
module.exports = OrderByAndLimitExp;
export = OrderByAndLimitExp;

View File

@ -1,16 +1,19 @@
"use strict";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
import Expression = require('./expression');
import NoteSet = require('../note_set');
import SearchContext = require('../search_context');
class ParentOfExp extends Expression {
constructor(subExpression) {
private subExpression: Expression;
constructor(subExpression: Expression) {
super();
this.subExpression = subExpression;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const subInputNoteSet = new NoteSet();
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";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
const buildComparator = require('../services/build_comparator.js');
import Expression = require('./expression');
import NoteSet = require('../note_set');
import buildComparator = require('../services/build_comparator');
/**
* 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.
*/
const PROP_MAPPING = {
const PROP_MAPPING: Record<string, string> = {
"noteid": "noteId",
"title": "title",
"type": "type",
@ -36,12 +36,22 @@ const PROP_MAPPING = {
"revisioncount": "revisionCount"
};
interface SearchContext {
dbLoadNeeded?: boolean;
}
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;
}
constructor(searchContext, propertyName, operator, comparedValue) {
constructor(searchContext: SearchContext, propertyName: string, operator: string, comparedValue: string) {
super();
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();
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') {
value = value.toString();
@ -68,7 +78,7 @@ class PropertyComparisonExp extends Expression {
value = value.toLowerCase();
}
if (this.comparator(value)) {
if (this.comparator && this.comparator(value)) {
resNoteSet.add(note);
}
}
@ -77,4 +87,4 @@ class PropertyComparisonExp extends Expression {
}
}
module.exports = PropertyComparisonExp;
export = PropertyComparisonExp;

View File

@ -1,18 +1,22 @@
"use strict";
const Expression = require('./expression.js');
const NoteSet = require('../note_set');
const becca = require('../../../becca/becca');
import Expression = require('./expression');
import NoteSet = require('../note_set');
import becca = require('../../../becca/becca');
import SearchContext = require('../search_context');
class RelationWhereExp extends Expression {
constructor(relationName, subExpression) {
private relationName: string;
private subExpression: Expression;
constructor(relationName: string, subExpression: Expression) {
super();
this.relationName = relationName;
this.subExpression = subExpression;
}
execute(inputNoteSet, executionContext, searchContext) {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const candidateNoteSet = new NoteSet();
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";
const hoistedNoteService = require('../hoisted_note.js');
import hoistedNoteService = require('../hoisted_note');
import { SearchParams } from './services/types';
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.includeArchivedNotes = !!params.includeArchivedNotes;
this.includeHiddenNotes = !!params.includeHiddenNotes;
@ -32,7 +52,7 @@ class SearchContext {
this.error = null;
}
addError(error) {
addError(error: string) {
// we record only the first error, subsequent ones are usually a consequence of the first
if (!this.error) {
this.error = error;
@ -48,4 +68,4 @@ class SearchContext {
}
}
module.exports = SearchContext;
export = SearchContext;

View File

@ -1,12 +1,18 @@
"use strict";
const beccaService = require('../../becca/becca_service');
const becca = require('../../becca/becca');
import beccaService = require('../../becca/becca_service');
import becca = require('../../becca/becca');
class SearchResult {
constructor(notePathArray) {
notePathArray: string[];
score: number;
notePathTitle: string;
highlightedNotePathTitle?: string;
constructor(notePathArray: string[]) {
this.notePathArray = notePathArray;
this.notePathTitle = beccaService.getNoteTitleForPath(notePathArray);
this.score = 0;
}
get notePath() {
@ -17,7 +23,7 @@ class SearchResult {
return this.notePathArray[this.notePathArray.length - 1];
}
computeScore(fulltextQuery, tokens) {
computeScore(fulltextQuery: string, tokens: string[]) {
this.score = 0;
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(" ");
this.score = 0;
for (const chunk of chunks) {
for (const token of tokens) {
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)) {
cachedRegexes[str] = new RegExp(str);
}
@ -8,31 +8,36 @@ function getRegex(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 && val.endsWith(comparedValue)),
"=*": comparedValue => (val => val && val.startsWith(comparedValue)),
"*=*": comparedValue => (val => val && val.includes(comparedValue)),
"%=": comparedValue => (val => val && !!getRegex(comparedValue).test(val)),
"*=": comparedValue => (val => !!val && val.endsWith(comparedValue)),
"=*": comparedValue => (val => !!val && val.startsWith(comparedValue)),
"*=*": comparedValue => (val => !!val && val.includes(comparedValue)),
"%=": 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)
};
function buildComparator(operator, comparedValue) {
function buildComparator(operator: string, comparedValue: string) {
comparedValue = comparedValue.toLowerCase();
if (operator in numericComparators && !isNaN(comparedValue)) {
return numericComparators[operator](parseFloat(comparedValue));
if (operator in numericComparators) {
const floatValue = parseFloat(comparedValue);
if (!isNaN(floatValue)) {
return numericComparators[operator](floatValue);
}
}
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
*/
function handleParens(tokens) {
function handleParens(tokens: (TokenData | TokenData[])[]) {
if (tokens.length === 0) {
return [];
}
while (true) {
const leftIdx = tokens.findIndex(token => token.token === '(');
const leftIdx = tokens.findIndex(token => "token" in token && token.token === '(');
if (leftIdx === -1) {
return tokens;
@ -17,13 +19,18 @@ function handleParens(tokens) {
let parensLevel = 0
for (rightIdx = leftIdx; rightIdx < tokens.length; rightIdx++) {
if (tokens[rightIdx].token === ')') {
const token = tokens[rightIdx];
if (!("token" in token)) {
continue;
}
if (token.token === ')') {
parensLevel--;
if (parensLevel === 0) {
break;
}
} else if (tokens[rightIdx].token === '(') {
} else if (token.token === '(') {
parensLevel++;
}
}
@ -36,8 +43,8 @@ function handleParens(tokens) {
...tokens.slice(0, leftIdx),
handleParens(tokens.slice(leftIdx + 1, rightIdx)),
...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();
let fulltextQuery = "";
const fulltextTokens = [];
const expressionTokens = [];
const fulltextTokens: TokenData[] = [];
const expressionTokens: TokenData[] = [];
/** @type {boolean|string} */
let quotes = false; // otherwise contains used quote - ', " or `
let quotes: boolean | string = false; // otherwise contains used quote - ', " or `
let fulltextEnded = false;
let currentWord = '';
function isSymbolAnOperator(chr) {
function isSymbolAnOperator(chr: string) {
return ['=', '*', '>', '<', '!', "-", "+", '%', ','].includes(chr);
}
@ -23,12 +24,12 @@ function lex(str) {
}
}
function finishWord(endIndex, createAlsoForEmptyWords = false) {
function finishWord(endIndex: number, createAlsoForEmptyWords = false) {
if (currentWord === '' && !createAlsoForEmptyWords) {
return;
}
const rec = {
const rec: TokenData = {
token: currentWord,
inQuotes: !!quotes,
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";
const dayjs = require("dayjs");
const AndExp = require('../expressions/and.js');
const OrExp = require('../expressions/or.js');
const NotExp = require('../expressions/not.js');
const ChildOfExp = require('../expressions/child_of.js');
const DescendantOfExp = require('../expressions/descendant_of.js');
const ParentOfExp = require('../expressions/parent_of.js');
const RelationWhereExp = require('../expressions/relation_where.js');
const PropertyComparisonExp = require('../expressions/property_comparison.js');
const AttributeExistsExp = require('../expressions/attribute_exists.js');
const LabelComparisonExp = require('../expressions/label_comparison.js');
const NoteFlatTextExp = require('../expressions/note_flat_text.js');
const NoteContentFulltextExp = require('../expressions/note_content_fulltext.js');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit.js');
const AncestorExp = require('../expressions/ancestor.js');
const buildComparator = require('./build_comparator.js');
const ValueExtractor = require('../value_extractor.js');
const utils = require('../../utils');
const TrueExp = require('../expressions/true.js');
const IsHiddenExp = require('../expressions/is_hidden.js');
import dayjs = require("dayjs");
import AndExp = require('../expressions/and');
import OrExp = require('../expressions/or');
import NotExp = require('../expressions/not');
import ChildOfExp = require('../expressions/child_of');
import DescendantOfExp = require('../expressions/descendant_of');
import ParentOfExp = require('../expressions/parent_of');
import RelationWhereExp = require('../expressions/relation_where');
import PropertyComparisonExp = require('../expressions/property_comparison');
import AttributeExistsExp = require('../expressions/attribute_exists');
import LabelComparisonExp = require('../expressions/label_comparison');
import NoteFlatTextExp = require('../expressions/note_flat_text');
import NoteContentFulltextExp = require('../expressions/note_content_fulltext');
import OrderByAndLimitExp = require('../expressions/order_by_and_limit');
import AncestorExp = require('../expressions/ancestor');
import buildComparator = require('./build_comparator');
import ValueExtractor = require('../value_extractor');
import utils = require('../../utils');
import TrueExp = require('../expressions/true');
import IsHiddenExp = require('../expressions/is_hidden');
import SearchContext = require("../search_context");
import { TokenData } from "./types";
import Expression = require("../expressions/expression");
function getFulltext(tokens, searchContext) {
tokens = tokens.map(t => utils.removeDiacritic(t.token));
function getFulltext(_tokens: TokenData[], searchContext: SearchContext) {
const tokens: string[] = _tokens.map(t => utils.removeDiacritic(t.token));
searchContext.highlightedTokens.push(...tokens);
@ -54,7 +57,7 @@ const OPERATORS = [
"%="
];
function isOperator(token) {
function isOperator(token: TokenData) {
if (Array.isArray(token)) {
return false;
}
@ -62,20 +65,20 @@ function isOperator(token) {
return OPERATORS.includes(token.token);
}
function getExpression(tokens, searchContext, level = 0) {
function getExpression(tokens: TokenData[], searchContext: SearchContext, level = 0) {
if (tokens.length === 0) {
return null;
}
const expressions = [];
let op = null;
const expressions: Expression[] = [];
let op: string | null = null;
let i;
let i: number;
function context(i) {
function context(i: number) {
let {startIndex, endIndex} = tokens[i];
startIndex = Math.max(0, startIndex - 20);
endIndex = Math.min(searchContext.originalQuery.length, endIndex + 20);
startIndex = Math.max(0, (startIndex || 0) - 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 ? "..." : ""}"`;
}
@ -133,7 +136,7 @@ function getExpression(tokens, searchContext, level = 0) {
return date.format(format);
}
function parseNoteProperty() {
function parseNoteProperty(): Expression | undefined | null {
if (tokens[i].token !== '.') {
searchContext.addError('Expected "." to separate field path');
return;
@ -161,19 +164,25 @@ function getExpression(tokens, searchContext, level = 0) {
if (tokens[i].token === 'parents') {
i += 1;
return new ChildOfExp(parseNoteProperty());
const expression = parseNoteProperty();
if (!expression) { return; }
return new ChildOfExp(expression);
}
if (tokens[i].token === 'children') {
i += 1;
return new ParentOfExp(parseNoteProperty());
const expression = parseNoteProperty();
if (!expression) { return; }
return new ParentOfExp(expression);
}
if (tokens[i].token === 'ancestors') {
i += 1;
return new DescendantOfExp(parseNoteProperty());
const expression = parseNoteProperty();
if (!expression) { return; }
return new DescendantOfExp(expression);
}
if (tokens[i].token === 'labels') {
@ -219,6 +228,10 @@ function getExpression(tokens, searchContext, level = 0) {
i += 2;
const comparedValue = resolveConstantOperand();
if (!comparedValue) {
searchContext.addError(`Unresolved constant operand.`);
return;
}
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)}`);
}
function parseAttribute(name) {
function parseAttribute(name: string) {
const isLabel = name.startsWith('#');
name = name.substr(1);
@ -239,10 +252,10 @@ function getExpression(tokens, searchContext, level = 0) {
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);
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);
if (i < tokens.length - 2 && tokens[i + 1].token === '.') {
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])) {
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() {
const orderDefinitions = [];
const orderDefinitions: {
valueExtractor: ValueExtractor,
direction: string
}[] = [];
let limit;
if (tokens[i].token === 'orderby') {
@ -316,8 +334,9 @@ function getExpression(tokens, searchContext, level = 0) {
const valueExtractor = new ValueExtractor(searchContext, propertyPath);
if (valueExtractor.validate()) {
searchContext.addError(valueExtractor.validate());
const validationError = valueExtractor.validate();
if (validationError) {
searchContext.addError(validationError);
}
orderDefinitions.push({
@ -348,7 +367,10 @@ function getExpression(tokens, searchContext, level = 0) {
for (i = 0; i < tokens.length; 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;
}
@ -359,7 +381,10 @@ function getExpression(tokens, searchContext, level = 0) {
}
if (token.startsWith('#') || token.startsWith('~')) {
expressions.push(parseAttribute(token));
const attribute = parseAttribute(token);
if (attribute) {
expressions.push(attribute);
}
}
else if (['orderby', 'limit'].includes(token)) {
if (level !== 0) {
@ -384,12 +409,17 @@ function getExpression(tokens, searchContext, level = 0) {
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') {
i++;
expressions.push(parseNoteProperty());
const expression = parseNoteProperty();
if (!expression) { return; }
expressions.push(expression);
continue;
}
@ -416,13 +446,18 @@ function getExpression(tokens, searchContext, level = 0) {
return getAggregateExpression();
}
function parse({fulltextTokens, expressionTokens, searchContext}) {
let expression;
function parse({fulltextTokens, expressionTokens, searchContext}: {
fulltextTokens: TokenData[],
expressionTokens: (TokenData | TokenData[])[],
searchContext: SearchContext,
originalQuery: string
}) {
let expression: Expression | undefined | null;
try {
expression = getExpression(expressionTokens, searchContext);
expression = getExpression(expressionTokens as TokenData[], searchContext);
}
catch (e) {
catch (e: any) {
searchContext.addError(e.message);
expression = new TrueExp();
@ -441,15 +476,15 @@ function parse({fulltextTokens, expressionTokens, searchContext}) {
exp = new OrderByAndLimitExp([{
valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]),
direction: searchContext.orderDirection
}], searchContext.limit);
}], searchContext.limit || undefined);
exp.subExpression = filterExp;
(exp as any).subExpression = filterExp;
}
return exp;
}
function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}) {
function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}: SearchContext) {
if (ancestorNoteId && ancestorNoteId !== 'root') {
return new AncestorExp(ancestorNoteId, ancestorDepth);
} 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";
const normalizeString = require("normalize-strings");
const lex = require('./lex.js');
const handleParens = require('./handle_parens.js');
const parse = require('./parse.js');
const SearchResult = require('../search_result.js');
const SearchContext = require('../search_context.js');
const becca = require('../../../becca/becca');
const beccaService = require('../../../becca/becca_service');
const utils = require('../../utils');
const log = require('../../log');
const hoistedNoteService = require('../../hoisted_note.js');
import normalizeString = require("normalize-strings");
import lex = require('./lex');
import handleParens = require('./handle_parens');
import parse = require('./parse');
import SearchResult = require('../search_result');
import SearchContext = require('../search_context');
import becca = require('../../../becca/becca');
import beccaService = require('../../../becca/becca_service');
import utils = require('../../utils');
import log = require('../../log');
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) {
let searchResultNoteIds, highlightedTokens;
function searchFromNote(note: BNote) {
let searchResultNoteIds;
let highlightedTokens: string[];
const searchScript = note.getRelationValue('searchScript');
const searchString = note.getLabelValue('searchString');
const searchString = note.getLabelValue('searchString') || "";
let error = null;
if (searchScript) {
@ -25,12 +31,12 @@ function searchFromNote(note) {
} else {
const searchContext = new SearchContext({
fastSearch: note.hasLabel('fastSearch'),
ancestorNoteId: note.getRelationValue('ancestor'),
ancestorDepth: note.getLabelValue('ancestorDepth'),
ancestorNoteId: note.getRelationValue('ancestor') || undefined,
ancestorDepth: note.getLabelValue('ancestorDepth') || undefined,
includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
orderBy: note.getLabelValue('orderBy'),
orderDirection: note.getLabelValue('orderDirection'),
limit: note.getLabelValue('limit'),
orderBy: note.getLabelValue('orderBy') || undefined,
orderDirection: note.getLabelValue('orderDirection') || undefined,
limit: parseInt(note.getLabelValue('limit') || "0", 10),
debug: note.hasLabel('debug'),
fuzzyAttributeSearch: false
});
@ -51,7 +57,7 @@ function searchFromNote(note) {
};
}
function searchFromRelation(note, relationName) {
function searchFromRelation(note: BNote, relationName: string) {
const scriptNote = note.getRelationTarget(relationName);
if (!scriptNote) {
@ -90,18 +96,21 @@ function searchFromRelation(note, relationName) {
}
function loadNeededInfoFromDatabase() {
const sql = require('../../sql');
/**
* 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
* 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
noteId,
blobId,
@ -122,7 +131,12 @@ function loadNeededInfoFromDatabase() {
noteBlobs[noteId] = { [blobId]: length };
}
const attachmentContentLengths = sql.getRows(`
type AttachmentContentLengthsRow = {
noteId: string;
blobId: string;
length: number;
};
const attachmentContentLengths = sql.getRows<AttachmentContentLengthsRow>(`
SELECT
ownerId AS noteId,
attachments.blobId,
@ -151,7 +165,13 @@ function loadNeededInfoFromDatabase() {
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
noteId,
revisions.blobId,
@ -187,7 +207,10 @@ function loadNeededInfoFromDatabase() {
noteBlobs[noteId][blobId] = length;
if (isNoteRevision) {
becca.notes[noteId].revisionCount++;
const noteRevision = becca.notes[noteId];
if (noteRevision && noteRevision.revisionCount) {
noteRevision.revisionCount++;
}
}
}
@ -196,20 +219,16 @@ function loadNeededInfoFromDatabase() {
}
}
/**
* @param {Expression} expression
* @param {SearchContext} searchContext
* @returns {SearchResult[]}
*/
function findResultsWithExpression(expression, searchContext) {
function findResultsWithExpression(expression: Expression, searchContext: SearchContext): SearchResult[] {
if (searchContext.dbLoadNeeded) {
loadNeededInfoFromDatabase();
}
const allNoteSet = becca.getAllNoteSet();
const noteIdToNotePath: Record<string, string[]> = {};
const executionContext = {
noteIdToNotePath: {}
noteIdToNotePath
};
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
@ -250,16 +269,16 @@ function findResultsWithExpression(expression, searchContext) {
return searchResults;
}
function parseQueryToExpression(query, searchContext) {
function parseQueryToExpression(query: string, searchContext: SearchContext) {
const {fulltextQuery, fulltextTokens, expressionTokens} = lex(query);
searchContext.fulltextQuery = fulltextQuery;
let structuredExpressionTokens;
let structuredExpressionTokens: (TokenData | TokenData[])[];
try {
structuredExpressionTokens = handleParens(expressionTokens);
}
catch (e) {
catch (e: any) {
structuredExpressionTokens = [];
searchContext.addError(e.message);
}
@ -284,23 +303,13 @@ function parseQueryToExpression(query, searchContext) {
return expression;
}
/**
* @param {string} query
* @param {object} params - see SearchContext
* @returns {BNote[]}
*/
function searchNotes(query, params = {}) {
function searchNotes(query: string, params: SearchParams = {}): BNote[] {
const searchResults = findResultsWithQuery(query, new SearchContext(params));
return searchResults.map(sr => becca.notes[sr.noteId]);
}
/**
* @param {string} query
* @param {SearchContext} searchContext
* @returns {SearchResult[]}
*/
function findResultsWithQuery(query, searchContext) {
function findResultsWithQuery(query: string, searchContext: SearchContext): SearchResult[] {
query = query || "";
searchContext.originalQuery = query;
@ -313,18 +322,13 @@ function findResultsWithQuery(query, searchContext) {
return findResultsWithExpression(expression, searchContext);
}
/**
* @param {string} query
* @param {SearchContext} searchContext
* @returns {BNote|null}
*/
function findFirstNoteWithQuery(query, searchContext) {
function findFirstNoteWithQuery(query: string, searchContext: SearchContext): BNote | null {
const searchResults = findResultsWithQuery(query, searchContext);
return searchResults.length > 0 ? becca.notes[searchResults[0].noteId] : null;
}
function searchNotesForAutocomplete(query) {
function searchNotesForAutocomplete(query: string) {
const searchContext = new SearchContext({
fastSearch: true,
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));
// 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);
}
@ -403,6 +407,7 @@ function highlightSearchResults(searchResults, highlightedTokens) {
let match;
// Find all matches
if (!result.highlightedNotePathTitle) { continue; }
while ((match = tokenRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) {
result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}");
@ -413,6 +418,7 @@ function highlightSearchResults(searchResults, highlightedTokens) {
}
for (const result of searchResults) {
if (!result.highlightedNotePathTitle) { continue; }
result.highlightedNotePathTitle = result.highlightedNotePathTitle
.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') {
return `~${utils.escapeHtml(attr.name)}=…`;
}
@ -438,7 +444,7 @@ function formatAttribute(attr) {
}
}
module.exports = {
export = {
searchFromNote,
searchNotesForAutocomplete,
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";
import BNote = require("../../becca/entities/bnote");
/**
* 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.
*/
const PROP_MAPPING = {
const PROP_MAPPING: Record<string, string> = {
"noteid": "noteId",
"title": "title",
"type": "type",
@ -32,8 +34,14 @@ const PROP_MAPPING = {
"revisioncount": "revisionCount"
};
interface SearchContext {
dbLoadNeeded: boolean;
}
class ValueExtractor {
constructor(searchContext, propertyPath) {
private propertyPath: string[];
constructor(searchContext: SearchContext, propertyPath: string[]) {
this.propertyPath = propertyPath.map(pathEl => pathEl.toLowerCase());
if (this.propertyPath[0].startsWith('#')) {
@ -81,10 +89,10 @@ class ValueExtractor {
}
}
extract(note) {
let cursor = note;
extract(note: BNote) {
let cursor: BNote | null = note;
let i;
let i: number = 0;
const cur = () => this.propertyPath[i];
@ -105,8 +113,7 @@ class ValueExtractor {
i++;
const attr = cursor.getAttributeCaseInsensitive('relation', cur());
cursor = attr ? attr.targetNote : null;
cursor = attr?.targetNote || null;
}
else if (cur() === 'parents') {
cursor = cursor.parents[0];
@ -118,7 +125,7 @@ class ValueExtractor {
return Math.random().toString(); // string is expected for comparison
}
else if (cur() in PROP_MAPPING) {
return cursor[PROP_MAPPING[cur()]];
return (cursor as any)[PROP_MAPPING[cur()]];
}
else {
// FIXME
@ -127,4 +134,4 @@ class ValueExtractor {
}
}
module.exports = ValueExtractor;
export = ValueExtractor;

View File

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

View File

@ -1,12 +1,12 @@
const attributeService = require('./attributes.js');
const dateNoteService = require('./date_notes.js');
const attributeService = require('./attributes');
const dateNoteService = require('./date_notes');
const becca = require('../becca/becca');
const noteService = require('./notes');
const dateUtils = require('./date_utils');
const log = require('./log');
const hoistedNoteService = require('./hoisted_note.js');
const searchService = require('./search/services/search.js');
const SearchContext = require('./search/search_context.js');
const hoistedNoteService = require('./hoisted_note');
const searchService = require('./search/services/search');
const SearchContext = require('./search/search_context');
const {LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT} = require('./hidden_subtree');
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[]) || [];
}
function iterateRows(query: string, params: Params = []) {
function iterateRows<T>(query: string, params: Params = []): IterableIterator<T> {
if (LOG_ALL_QUERIES) {
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 = []) {

View File

@ -84,7 +84,7 @@ async function createInitialDatabase() {
notePosition: 10
}).save();
const optionsInitService = require('./options_init.js');
const optionsInitService = require('./options_init');
optionsInitService.initDocumentOptions();
optionsInitService.initNotSyncedOptions(true, {});
@ -132,7 +132,7 @@ function createDatabaseForSync(options: OptionRow[], syncServerHost = '', syncPr
sql.transactional(() => {
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
for (const opt of options) {

View File

@ -1,27 +1,50 @@
"use strict";
const log = require('./log');
const sql = require('./sql');
const optionService = require('./options');
const utils = require('./utils');
const instanceId = require('./instance_id');
const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update.js');
const contentHashService = require('./content_hash.js');
const appInfo = require('./app_info');
const syncOptions = require('./sync_options');
const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
const request = require('./request');
const ws = require('./ws');
const entityChangesService = require('./entity_changes');
const entityConstructor = require('../becca/entity_constructor');
const becca = require('../becca/becca');
import log = require('./log');
import sql = require('./sql');
import optionService = require('./options');
import utils = require('./utils');
import instanceId = require('./instance_id');
import dateUtils = require('./date_utils');
import syncUpdateService = require('./sync_update');
import contentHashService = require('./content_hash');
import appInfo = require('./app_info');
import syncOptions = require('./sync_options');
import syncMutexService = require('./sync_mutex');
import cls = require('./cls');
import request = require('./request');
import ws = require('./ws');
import entityChangesService = require('./entity_changes');
import entityConstructor = require('../becca/entity_constructor');
import becca = require('../becca/becca');
import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface';
import { CookieJar, ExecOpts } from './request_interface';
let proxyToggle = true;
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() {
try {
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
proxyToggle = !proxyToggle;
@ -93,19 +116,23 @@ async function login() {
return await doLogin();
}
async function doLogin() {
async function doLogin(): Promise<SyncContext> {
const timestamp = dateUtils.utcNowDateTime();
const documentSecret = optionService.getOption('documentSecret');
const hash = utils.hmac(documentSecret, timestamp);
const syncContext = { cookieJar: {} };
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
const syncContext: SyncContext = { cookieJar: {} };
const resp = await syncRequest<SyncResponse>(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp,
syncVersion: appInfo.syncVersion,
hash: hash
});
if (!resp) {
throw new Error("Got no response.");
}
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.`);
}
@ -125,7 +152,7 @@ async function doLogin() {
return syncContext;
}
async function pullChanges(syncContext) {
async function pullChanges(syncContext: SyncContext) {
while (true) {
const lastSyncedPull = getLastSyncedPull();
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 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;
outstandingPullCount = resp.outstandingPullCount;
@ -141,7 +171,9 @@ async function pullChanges(syncContext) {
const pulledDate = Date.now();
sql.transactional(() => {
syncUpdateService.updateEntities(entityChanges, syncContext.instanceId);
if (syncContext.instanceId) {
syncUpdateService.updateEntities(entityChanges, syncContext.instanceId);
}
if (lastSyncedPull !== 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`);
}
catch (e) {
catch (e: any) {
log.error(`Error occurred ${e.message} ${e.stack}`);
}
}
@ -165,11 +197,11 @@ async function pullChanges(syncContext) {
log.info("Finished pull");
}
async function pushChanges(syncContext) {
let lastSyncedPush = getLastSyncedPush();
async function pushChanges(syncContext: SyncContext) {
let lastSyncedPush: number | null | undefined = getLastSyncedPush();
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) {
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
// has been filtered out
setLastSyncedPush(lastSyncedPush);
@ -214,16 +246,22 @@ async function pushChanges(syncContext) {
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');
}
async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
async function checkContentHash(syncContext: SyncContext) {
const resp = await syncRequest<CheckResponse>(syncContext, 'GET', '/api/sync/check');
if (!resp) {
throw new Error("Got no response.");
}
const lastSyncedPullId = getLastSyncedPull();
if (lastSyncedPullId < resp.maxEntityChangeId) {
@ -261,8 +299,12 @@ async function checkContentHash(syncContext) {
const PAGE_SIZE = 1000000;
async function syncRequest(syncContext, method, requestPath, body) {
body = body ? JSON.stringify(body) : '';
interface SyncContext {
cookieJar: CookieJar
}
async function syncRequest<T extends {}>(syncContext: SyncContext, method: string, requestPath: string, _body?: {}) {
const body = _body ? JSON.stringify(_body) : '';
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));
for (let pageIndex = 0; pageIndex < pageCount; pageIndex++) {
const opts = {
const opts: ExecOpts = {
method,
url: syncOptions.getSyncServerHost() + requestPath,
cookieJar: syncContext.cookieJar,
@ -286,13 +328,13 @@ async function syncRequest(syncContext, method, requestPath, body) {
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;
}
function getEntityChangeRow(entityChange) {
function getEntityChangeRow(entityChange: EntityChange) {
const {entityName, entityId} = entityChange;
if (entityName === 'note_reordering') {
@ -305,7 +347,7 @@ function getEntityChangeRow(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) {
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 = entityRow.content.toString("base64");
if (entityRow.content) {
entityRow.content = entityRow.content.toString("base64");
}
}
return entityRow;
}
}
function getEntityChangeRecords(entityChanges) {
const records = [];
function getEntityChangeRecords(entityChanges: EntityChange[]) {
const records: EntityChangeRecord[] = [];
let length = 0;
for (const entityChange of entityChanges) {
@ -340,7 +384,7 @@ function getEntityChangeRecords(entityChanges) {
continue;
}
const record = { entityChange, entity };
const record: EntityChangeRecord = { entityChange, entity };
records.push(record);
@ -359,7 +403,7 @@ function getLastSyncedPull() {
return parseInt(optionService.getOption('lastSyncedPull'));
}
function setLastSyncedPull(entityChangeId) {
function setLastSyncedPull(entityChangeId: number) {
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
@ -378,7 +422,7 @@ function getLastSyncedPush() {
return lastSyncedPush;
}
function setLastSyncedPush(entityChangeId) {
function setLastSyncedPush(entityChangeId: number) {
ws.setLastSyncedPush(entityChangeId);
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
@ -409,7 +453,7 @@ require('../becca/becca_loader').beccaLoaded.then(() => {
getLastSyncedPush();
});
module.exports = {
export = {
sync,
login,
getEntityChangeRecords,

View File

@ -1,11 +1,18 @@
const sql = require('./sql');
const log = require('./log');
const entityChangesService = require('./entity_changes');
const eventService = require('./events');
const entityConstructor = require('../becca/entity_constructor');
const ws = require('./ws');
import sql = require('./sql');
import log = require('./log');
import entityChangesService = require('./entity_changes');
import eventService = require('./events');
import entityConstructor = require('../becca/entity_constructor');
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) {
return;
}
@ -34,13 +41,15 @@ function updateEntities(entityChanges, instanceId) {
atLeastOnePullApplied = true;
}
updateEntity(entityChange, entity, instanceId, updateContext);
if (entity) {
updateEntity(entityChange, entity, instanceId, updateContext);
}
}
logUpdateContext(updateContext);
}
function updateEntity(remoteEC, remoteEntityRow, instanceId, updateContext) {
function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string, updateContext: UpdateContext) {
if (!remoteEntityRow && remoteEC.entityName === 'options') {
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) {
const localEC = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string, updateContext: UpdateContext) {
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 (remoteEC.isErased) {
@ -110,28 +123,30 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
return false;
}
function preProcessContent(remoteEC, remoteEntityRow) {
function preProcessContent(remoteEC: EntityChange, remoteEntityRow: EntityRow) {
if (remoteEC.entityName === 'blobs' && remoteEntityRow.content !== null) {
// we always use a Buffer object which is different from normal saving - there we use a simple string type for
// "string notes". The problem is that in general, it's not possible to detect whether a blob content
// is string note or note (syncs can arrive out of order)
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
if (typeof remoteEntityRow.content === "string") {
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
if (remoteEntityRow.content.byteLength === 0) {
// there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
// (possibly not a problem anymore with the newer better-sqlite3)
remoteEntityRow.content = "";
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
// (possibly not a problem anymore with the newer better-sqlite3)
remoteEntityRow.content = "";
}
}
}
}
function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) {
function updateNoteReordering(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string) {
if (!remoteEntityRow) {
throw new Error(`Empty note_reordering body for: ${JSON.stringify(remoteEC)}`);
}
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);
@ -139,7 +154,7 @@ function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) {
return true;
}
function eraseEntity(entityChange) {
function eraseEntity(entityChange: EntityChange) {
const {entityName, entityId} = entityChange;
const entityNames = [
@ -161,7 +176,7 @@ function eraseEntity(entityChange) {
sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]);
}
function logUpdateContext(updateContext) {
function logUpdateContext(updateContext: UpdateContext) {
const message = JSON.stringify(updateContext)
.replaceAll('"', '')
.replaceAll(":", ": ")
@ -170,6 +185,6 @@ function logUpdateContext(updateContext) {
log.info(message.substr(1, message.length - 2));
}
module.exports = {
export = {
updateEntities
};

View File

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

View File

@ -1,12 +1,13 @@
"use strict";
const sql = require('./sql');
const log = require('./log');
const BBranch = require('../becca/entities/bbranch');
const entityChangesService = require('./entity_changes');
const becca = require('../becca/becca');
import sql = require('./sql');
import log = require('./log');
import BBranch = require('../becca/entities/bbranch');
import entityChangesService = require('./entity_changes');
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)) {
return { branch: null, success: false, message: `Cannot change this note's location.` };
}
@ -25,7 +26,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
return {
branch: existingBranch,
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 {
branch: null,
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.
*/
function wouldAddingBranchCreateCycle(parentNoteId, childNoteId) {
function wouldAddingBranchCreateCycle(parentNoteId: string, childNoteId: string) {
if (parentNoteId === childNoteId) {
return true;
}
@ -70,20 +71,22 @@ function wouldAddingBranchCreateCycle(parentNoteId, childNoteId) {
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) {
customSortBy = 'title';
}
if (!sortLocale) {
// sortLocale can not be empty string or null value, default value must be set to undefined.
sortLocale = undefined;
}
// sortLocale can not be empty string or null value, default value must be set to undefined.
const sortLocale = (_sortLocale || undefined);
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) => {
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;
if (key === 'title') {
@ -105,14 +108,14 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
rawValue = prefix ? `${prefix} - ${note.title}` : note.title;
} else {
rawValue = ['dateCreated', 'dateModified'].includes(key)
? note[key]
? (note as any)[key]
: note.getLabelValue(key);
}
return normalize(rawValue);
}
function compare(a, b) {
function compare(a: string, b: string) {
if (!sortNatural) {
// alphabetical sort
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) {
const branch = note.getParentBranches().find(b => b.parentNoteId === parentNoteId);
if (!branch) { continue; }
if (branch.noteId === '_hidden') {
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);
if (!parentNote) {
return;
}
@ -206,7 +209,7 @@ function sortNotesIfNeeded(parentNoteId) {
/**
* @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);
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
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);
if (branch) {
@ -233,12 +236,15 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
}
else if (parentNoteId) {
const note = becca.getNote(noteId);
if (!note) {
throw new Error(`Cannot find note '${noteId}.`);
}
if (note.isDeleted) {
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);
if (branch) {
@ -255,7 +261,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
}
}
module.exports = {
export = {
validateParentChild,
sortNotes,
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
return promise;
}

View File

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

View File

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

View File

@ -6,9 +6,9 @@
require('../becca/entity_constructor');
const sqlInit = require('../services/sql_init');
const noteService = require('../services/notes');
const attributeService = require('../services/attributes.js');
const attributeService = require('../services/attributes');
const cls = require('../services/cls');
const cloningService = require('../services/cloning.js');
const cloningService = require('../services/cloning');
const loremIpsum = require('lorem-ipsum').loremIpsum;
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;
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
* 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.
*
* 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 => {
if (!listenOnTcp || error.syscall !== 'listen') {
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;
}
if (!listenOnTcp || error.syscall !== 'listen') {
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', () => {