refactor(server): typed options

This commit is contained in:
Elian Doran 2025-01-03 17:54:05 +02:00
parent 2590a4cb05
commit b6e97c1ae9
No known key found for this signature in database
6 changed files with 129 additions and 28 deletions

View File

@ -1,6 +1,6 @@
"use strict"; "use strict";
import optionService from "../../services/options.js"; import optionService, { OptionNames } from "../../services/options.js";
import log from "../../services/log.js"; import log from "../../services/log.js";
import searchService from "../../services/search/services/search.js"; import searchService from "../../services/search/services/search.js";
import ValidationError from "../../errors/validation_error.js"; import ValidationError from "../../errors/validation_error.js";
@ -79,7 +79,7 @@ function getOptions() {
for (const optionName in optionMap) { for (const optionName in optionMap) {
if (isAllowed(optionName)) { if (isAllowed(optionName)) {
resultMap[optionName] = optionMap[optionName]; resultMap[optionName] = optionMap[optionName as OptionNames];
} }
} }
@ -115,7 +115,7 @@ function update(name: string, value: string) {
log.info(`Updating option '${name}' to '${value}'`); log.info(`Updating option '${name}' to '${value}'`);
} }
optionService.setOption(name, value); optionService.setOption(name as OptionNames, value);
if (name === "locale") { if (name === "locale") {
// This runs asynchronously, so it's not perfect, but it does the trick for now. // This runs asynchronously, so it's not perfect, but it does the trick for now.

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
import dateUtils from "./date_utils.js"; import dateUtils from "./date_utils.js";
import optionService from "./options.js"; import optionService, { OptionNames } from "./options.js";
import fs from "fs-extra"; import fs from "fs-extra";
import dataDir from "./data_dir.js"; import dataDir from "./data_dir.js";
import log from "./log.js"; import log from "./log.js";
@ -38,12 +38,23 @@ function regularBackup() {
} }
function isBackupEnabled(backupType: BackupType) { function isBackupEnabled(backupType: BackupType) {
const optionName = `${backupType}BackupEnabled`; let optionName: OptionNames;
switch (backupType) {
case "daily":
optionName = "dailyBackupEnabled";
break;
case "weekly":
optionName = "weeklyBackupEnabled";
break;
case "monthly":
optionName = "monthlyBackupEnabled";
break;
}
return optionService.getOptionBool(optionName); return optionService.getOptionBool(optionName);
} }
function periodBackup(optionName: string, backupType: BackupType, periodInSeconds: number) { function periodBackup(optionName: "lastDailyBackupDate" | "lastWeeklyBackupDate" | "lastMonthlyBackupDate", backupType: BackupType, periodInSeconds: number) {
if (!isBackupEnabled(backupType)) { if (!isBackupEnabled(backupType)) {
return; return;
} }

View File

@ -92,7 +92,7 @@ const enum KeyboardActionNamesEnum {
forceSaveRevision forceSaveRevision
} }
type KeyboardActionNames = keyof typeof KeyboardActionNamesEnum; export type KeyboardActionNames = keyof typeof KeyboardActionNamesEnum;
export interface KeyboardShortcut { export interface KeyboardShortcut {
separator?: string; separator?: string;

View File

@ -15,14 +15,108 @@
import becca from "../becca/becca.js"; import becca from "../becca/becca.js";
import BOption from "../becca/entities/boption.js"; import BOption from "../becca/entities/boption.js";
import { OptionRow } from '../becca/entities/rows.js'; import { OptionRow } from '../becca/entities/rows.js';
import { KeyboardActionNames } from "./keyboard_actions_interface.js";
import sql from "./sql.js"; import sql from "./sql.js";
/**
* For each keyboard action, there is a corresponding option which identifies the key combination defined by the user.
*/
type KeyboardShortcutsOptions<T extends KeyboardActionNames> = {
[key in T as `keyboardShortcuts${Capitalize<key>}`]: string
};
interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActionNames> {
"openNoteContexts": string;
"lastDailyBackupDate": string;
"lastWeeklyBackupDate": string;
"lastMonthlyBackupDate": string;
"dbVersion": string;
"theme": string;
"syncServerHost": string;
"syncServerTimeout": string;
"syncProxy": string;
"mainFontFamily": string;
"treeFontFamily": string;
"detailFontFamily": string;
"monospaceFontFamily": string;
"spellCheckLanguageCode": string;
"codeNotesMimeTypes": string;
"headingStyle": string;
"highlightsList": string;
"customSearchEngineName": string;
"customSearchEngineUrl": string;
"locale": string;
"codeBlockTheme": string;
"textNoteEditorType": string;
"layoutOrientation": string;
"allowedHtmlTags": string;
"documentId": string;
"documentSecret": string;
"passwordVerificationHash": string;
"passwordVerificationSalt": string;
"passwordDerivedKeySalt": string;
"encryptedDataKey": string;
"lastSyncedPull": number;
"lastSyncedPush": number;
"revisionSnapshotTimeInterval": number;
"revisionSnapshotNumberLimit": number;
"protectedSessionTimeout": number;
"zoomFactor": number;
"mainFontSize": number;
"treeFontSize": number;
"detailFontSize": number;
"monospaceFontSize": number;
"imageMaxWidthHeight": number;
"imageJpegQuality": number;
"leftPaneWidth": number;
"rightPaneWidth": number;
"eraseEntitiesAfterTimeInSeconds": number;
"autoReadonlySizeText": number;
"autoReadonlySizeCode": number;
"maxContentWidth": number;
"minTocHeadings": number;
"eraseUnusedAttachmentsAfterSeconds": number;
"firstDayOfWeek": number;
"initialized": boolean;
"overrideThemeFonts": boolean;
"spellCheckEnabled": boolean;
"autoFixConsistencyIssues": boolean;
"vimKeymapEnabled": boolean;
"codeLineWrapEnabled": boolean;
"leftPaneVisible": boolean;
"rightPaneVisible": boolean;
"nativeTitleBarVisible": boolean;
"hideArchivedNotes_main": boolean;
"debugModeEnabled": boolean;
"autoCollapseNoteTree": boolean;
"dailyBackupEnabled": boolean;
"weeklyBackupEnabled": boolean;
"monthlyBackupEnabled": boolean;
"compressImages": boolean;
"downloadImagesAutomatically": boolean;
"checkForUpdates": boolean;
"disableTray": boolean;
"promotedAttributesOpenInRibbon": boolean;
"editedNotesOpenInRibbon": boolean;
"codeBlockWordWrap": boolean;
"textNoteEditorMultilineToolbar": boolean;
"backgroundEffects": boolean;
};
export type OptionNames = keyof OptionDefinitions;
type FilterOptionsByType<U> = {
[K in keyof OptionDefinitions]: OptionDefinitions[K] extends U ? K : never;
}[keyof OptionDefinitions];
/** /**
* A dictionary where the keys are the option keys (e.g. `theme`) and their corresponding values. * A dictionary where the keys are the option keys (e.g. `theme`) and their corresponding values.
*/ */
export type OptionMap = Record<string | number, string>; export type OptionMap = Record<OptionNames, string>;
function getOptionOrNull(name: string): string | null { function getOptionOrNull(name: OptionNames): string | null {
let option; let option;
if (becca.loaded) { if (becca.loaded) {
@ -35,7 +129,7 @@ function getOptionOrNull(name: string): string | null {
return option ? option.value : null; return option ? option.value : null;
} }
function getOption(name: string) { function getOption(name: OptionNames) {
const val = getOptionOrNull(name); const val = getOptionOrNull(name);
if (val === null) { if (val === null) {
@ -45,7 +139,7 @@ function getOption(name: string) {
return val; return val;
} }
function getOptionInt(name: string, defaultValue?: number): number { function getOptionInt(name: FilterOptionsByType<number>, defaultValue?: number): number {
const val = getOption(name); const val = getOption(name);
const intVal = parseInt(val); const intVal = parseInt(val);
@ -61,7 +155,7 @@ function getOptionInt(name: string, defaultValue?: number): number {
return intVal; return intVal;
} }
function getOptionBool(name: string): boolean { function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
const val = getOption(name); const val = getOption(name);
if (typeof val !== "string" || !['true', 'false'].includes(val)) { if (typeof val !== "string" || !['true', 'false'].includes(val)) {
@ -71,15 +165,11 @@ function getOptionBool(name: string): boolean {
return val === 'true'; return val === 'true';
} }
function setOption(name: string, value: string | number | boolean) { function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
if (value === true || value === false || typeof value === "number") {
value = value.toString();
}
const option = becca.getOption(name); const option = becca.getOption(name);
if (option) { if (option) {
option.value = value; option.value = value as string;
option.save(); option.save();
} }
@ -95,10 +185,10 @@ function setOption(name: string, value: string | number | boolean) {
* @param value the value of the option, as a string. It can then be interpreted as other types such as a number of boolean. * @param value the value of the option, as a string. It can then be interpreted as other types such as a number of boolean.
* @param isSynced `true` if the value should be synced across multiple instances (e.g. locale) or `false` if it should be local-only (e.g. theme). * @param isSynced `true` if the value should be synced across multiple instances (e.g. locale) or `false` if it should be local-only (e.g. theme).
*/ */
function createOption(name: string, value: string, isSynced: boolean) { function createOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T], isSynced: boolean) {
new BOption({ new BOption({
name: name, name: name,
value: value, value: value as string,
isSynced: isSynced isSynced: isSynced
}).save(); }).save();
} }
@ -108,13 +198,13 @@ function getOptions() {
} }
function getOptionMap() { function getOptionMap() {
const map: OptionMap = {}; const map: Record<string, string> = {};
for (const option of Object.values(becca.options)) { for (const option of Object.values(becca.options)) {
map[option.name] = option.value; map[option.name] = option.value;
} }
return map; return map as OptionMap;
} }
export default { export default {

View File

@ -1,5 +1,5 @@
import optionService from "./options.js"; import optionService from "./options.js";
import type { OptionMap } from "./options.js"; import type { OptionMap, OptionNames } from "./options.js";
import appInfo from "./app_info.js"; import appInfo from "./app_info.js";
import { randomSecureToken, isWindows } from "./utils.js"; import { randomSecureToken, isWindows } from "./utils.js";
import log from "./log.js"; import log from "./log.js";
@ -24,11 +24,11 @@ interface NotSyncedOpts {
* Represents a correspondence between an option and its default value, to be initialized when the database is missing that particular option (after a migration from an older version, or when creating a new database). * Represents a correspondence between an option and its default value, to be initialized when the database is missing that particular option (after a migration from an older version, or when creating a new database).
*/ */
interface DefaultOption { interface DefaultOption {
name: string; name: OptionNames;
/** /**
* The value to initialize the option with, if the option is not already present in the database. * The value to initialize the option with, if the option is not already present in the database.
* *
* If a function is passed in instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized. * If a function is passed Gin instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized.
*/ */
value: string | ((options: OptionMap) => string); value: string | ((options: OptionMap) => string);
isSynced: boolean; isSynced: boolean;
@ -194,7 +194,7 @@ function getKeyboardDefaultOptions() {
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`, name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
value: JSON.stringify(ka.defaultShortcuts), value: JSON.stringify(ka.defaultShortcuts),
isSynced: false isSynced: false
})); })) as DefaultOption[];
} }
export default { export default {

View File

@ -1,6 +1,6 @@
"use strict"; "use strict";
import optionService from "./options.js"; import optionService, { OptionNames } from "./options.js";
import config from "./config.js"; import config from "./config.js";
/* /*
@ -10,7 +10,7 @@ import config from "./config.js";
* to live sync server. * to live sync server.
*/ */
function get(name: string) { function get(name: OptionNames) {
return (config['Sync'] && config['Sync'][name]) || optionService.getOption(name); return (config['Sync'] && config['Sync'][name]) || optionService.getOption(name);
} }