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";
import optionService from "../../services/options.js";
import optionService, { OptionNames } from "../../services/options.js";
import log from "../../services/log.js";
import searchService from "../../services/search/services/search.js";
import ValidationError from "../../errors/validation_error.js";
@ -79,7 +79,7 @@ function getOptions() {
for (const optionName in optionMap) {
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}'`);
}
optionService.setOption(name, value);
optionService.setOption(name as OptionNames, value);
if (name === "locale") {
// This runs asynchronously, so it's not perfect, but it does the trick for now.

View File

@ -1,7 +1,7 @@
"use strict";
import dateUtils from "./date_utils.js";
import optionService from "./options.js";
import optionService, { OptionNames } from "./options.js";
import fs from "fs-extra";
import dataDir from "./data_dir.js";
import log from "./log.js";
@ -38,12 +38,23 @@ function regularBackup() {
}
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);
}
function periodBackup(optionName: string, backupType: BackupType, periodInSeconds: number) {
function periodBackup(optionName: "lastDailyBackupDate" | "lastWeeklyBackupDate" | "lastMonthlyBackupDate", backupType: BackupType, periodInSeconds: number) {
if (!isBackupEnabled(backupType)) {
return;
}

View File

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

View File

@ -15,14 +15,108 @@
import becca from "../becca/becca.js";
import BOption from "../becca/entities/boption.js";
import { OptionRow } from '../becca/entities/rows.js';
import { KeyboardActionNames } from "./keyboard_actions_interface.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.
*/
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;
if (becca.loaded) {
@ -35,7 +129,7 @@ function getOptionOrNull(name: string): string | null {
return option ? option.value : null;
}
function getOption(name: string) {
function getOption(name: OptionNames) {
const val = getOptionOrNull(name);
if (val === null) {
@ -45,7 +139,7 @@ function getOption(name: string) {
return val;
}
function getOptionInt(name: string, defaultValue?: number): number {
function getOptionInt(name: FilterOptionsByType<number>, defaultValue?: number): number {
const val = getOption(name);
const intVal = parseInt(val);
@ -61,7 +155,7 @@ function getOptionInt(name: string, defaultValue?: number): number {
return intVal;
}
function getOptionBool(name: string): boolean {
function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
const val = getOption(name);
if (typeof val !== "string" || !['true', 'false'].includes(val)) {
@ -71,15 +165,11 @@ function getOptionBool(name: string): boolean {
return val === 'true';
}
function setOption(name: string, value: string | number | boolean) {
if (value === true || value === false || typeof value === "number") {
value = value.toString();
}
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
const option = becca.getOption(name);
if (option) {
option.value = value;
option.value = value as string;
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 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({
name: name,
value: value,
value: value as string,
isSynced: isSynced
}).save();
}
@ -108,13 +198,13 @@ function getOptions() {
}
function getOptionMap() {
const map: OptionMap = {};
const map: Record<string, string> = {};
for (const option of Object.values(becca.options)) {
map[option.name] = option.value;
}
return map;
return map as OptionMap;
}
export default {

View File

@ -1,5 +1,5 @@
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 { randomSecureToken, isWindows } from "./utils.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).
*/
interface DefaultOption {
name: string;
name: OptionNames;
/**
* 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);
isSynced: boolean;
@ -194,7 +194,7 @@ function getKeyboardDefaultOptions() {
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
value: JSON.stringify(ka.defaultShortcuts),
isSynced: false
}));
})) as DefaultOption[];
}
export default {

View File

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