Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract preferences related server handlers from main.ts to server/preferences/app.ts #4420

Merged
merged 5 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 1 addition & 99 deletions packages/loot-core/src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { logger } from '../platform/server/log';
import * as sqlite from '../platform/server/sqlite';
import * as monthUtils from '../shared/months';
import { q } from '../shared/query';
import { stringToInteger } from '../shared/util';
import { type Budget } from '../types/budget';
import { Handlers } from '../types/handlers';
import { OpenIdConfig } from '../types/models/openid';
Expand Down Expand Up @@ -501,103 +500,6 @@ handlers['query'] = async function (query) {
return aqlQuery(query);
};

handlers['save-global-prefs'] = async function (prefs) {
if ('maxMonths' in prefs) {
await asyncStorage.setItem('max-months', '' + prefs.maxMonths);
}
if ('documentDir' in prefs) {
if (await fs.exists(prefs.documentDir)) {
await asyncStorage.setItem('document-dir', prefs.documentDir);
}
}
if ('floatingSidebar' in prefs) {
await asyncStorage.setItem('floating-sidebar', '' + prefs.floatingSidebar);
}
if ('language' in prefs) {
await asyncStorage.setItem('language', prefs.language);
}
if ('theme' in prefs) {
await asyncStorage.setItem('theme', prefs.theme);
}
if ('preferredDarkTheme' in prefs) {
await asyncStorage.setItem(
'preferred-dark-theme',
prefs.preferredDarkTheme,
);
}
if ('serverSelfSignedCert' in prefs) {
await asyncStorage.setItem(
'server-self-signed-cert',
prefs.serverSelfSignedCert,
);
}
return 'ok';
};

handlers['load-global-prefs'] = async function () {
const [
[, floatingSidebar],
[, maxMonths],
[, documentDir],
[, encryptKey],
[, language],
[, theme],
[, preferredDarkTheme],
[, serverSelfSignedCert],
] = await asyncStorage.multiGet([
'floating-sidebar',
'max-months',
'document-dir',
'encrypt-key',
'language',
'theme',
'preferred-dark-theme',
'server-self-signed-cert',
] as const);
return {
floatingSidebar: floatingSidebar === 'true' ? true : false,
maxMonths: stringToInteger(maxMonths || ''),
documentDir: documentDir || getDefaultDocumentDir(),
keyId: encryptKey && JSON.parse(encryptKey).id,
language,
theme:
theme === 'light' ||
theme === 'dark' ||
theme === 'auto' ||
theme === 'development' ||
theme === 'midnight'
? theme
: 'auto',
preferredDarkTheme:
preferredDarkTheme === 'dark' || preferredDarkTheme === 'midnight'
? preferredDarkTheme
: 'dark',
serverSelfSignedCert: serverSelfSignedCert || undefined,
};
};

handlers['save-prefs'] = async function (prefsToSet) {
const { cloudFileId } = prefs.getPrefs();

// Need to sync the budget name on the server as well
if (prefsToSet.budgetName && cloudFileId) {
const userToken = await asyncStorage.getItem('user-token');

await post(getServer().SYNC_SERVER + '/update-user-filename', {
token: userToken,
fileId: cloudFileId,
name: prefsToSet.budgetName,
});
}

await prefs.savePrefs(prefsToSet);
return 'ok';
};

handlers['load-prefs'] = async function () {
return prefs.getPrefs();
};

handlers['sync-reset'] = async function () {
return await resetSync();
};
Expand Down Expand Up @@ -1606,7 +1508,7 @@ app.combine(
accountsApp,
);

function getDefaultDocumentDir() {
export function getDefaultDocumentDir() {
if (Platform.isMobile) {
// On mobile, unfortunately we need to be backwards compatible
// with the old folder structure which does not store files inside
Expand Down
145 changes: 136 additions & 9 deletions packages/loot-core/src/server/preferences/app.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
import { type SyncedPrefs } from '../../types/prefs';
import * as asyncStorage from '../../platform/server/asyncStorage';
import * as fs from '../../platform/server/fs';
import { stringToInteger } from '../../shared/util';
import {
GlobalPrefs,
MetadataPrefs,
type SyncedPrefs,
} from '../../types/prefs';
import { createApp } from '../app';
import * as db from '../db';
import { getDefaultDocumentDir } from '../main';
import { mutator } from '../mutators';
import { post } from '../post';
import {
getPrefs as _getMetadataPrefs,
savePrefs as _saveMetadataPrefs,
} from '../prefs';
import { getServer } from '../server-config';
import { undoable } from '../undo';

import { PreferencesHandlers } from './types/handlers';
export interface PreferencesHandlers {
'preferences/save': typeof saveSyncedPrefs;
'preferences/get': typeof getSyncedPrefs;
'save-global-prefs': typeof saveGlobalPrefs;
'load-global-prefs': typeof loadGlobalPrefs;
'save-prefs': typeof saveMetadataPrefs;
'load-prefs': typeof loadMetadataPrefs;
}

export const app = createApp<PreferencesHandlers>();

const savePreferences = async ({
app.method('preferences/save', mutator(undoable(saveSyncedPrefs)));
app.method('preferences/get', getSyncedPrefs);
app.method('save-global-prefs', saveGlobalPrefs);
app.method('load-global-prefs', loadGlobalPrefs);
app.method('save-prefs', saveMetadataPrefs);
app.method('load-prefs', loadMetadataPrefs);

async function saveSyncedPrefs({
id,
value,
}: {
id: keyof SyncedPrefs;
value: string | undefined;
}) => {
}) {
await db.update('preferences', { id, value });
};
}

Comment on lines +39 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for database operations.

The function should handle potential database errors to prevent uncaught exceptions from propagating.

 async function saveSyncedPrefs({
   id,
   value,
 }: {
   id: keyof SyncedPrefs;
   value: string | undefined;
 }) {
+  try {
     await db.update('preferences', { id, value });
+  } catch (error) {
+    console.error('Failed to save synced preferences:', error);
+    throw new Error(`Failed to save preference ${id}: ${error.message}`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function saveSyncedPrefs({
id,
value,
}: {
id: keyof SyncedPrefs;
value: string | undefined;
}) => {
}) {
await db.update('preferences', { id, value });
};
}
async function saveSyncedPrefs({
id,
value,
}: {
id: keyof SyncedPrefs;
value: string | undefined;
}) {
try {
await db.update('preferences', { id, value });
} catch (error) {
console.error('Failed to save synced preferences:', error);
throw new Error(`Failed to save preference ${id}: ${error.message}`);
}
}

const getPreferences = async (): Promise<SyncedPrefs> => {
async function getSyncedPrefs(): Promise<SyncedPrefs> {
const prefs = (await db.all('SELECT id, value FROM preferences')) as Array<{
id: string;
value: string;
Expand All @@ -28,7 +56,106 @@ const getPreferences = async (): Promise<SyncedPrefs> => {
carry[id as keyof SyncedPrefs] = value;
return carry;
}, {});
};
}

async function saveGlobalPrefs(prefs: GlobalPrefs) {
if ('maxMonths' in prefs) {
await asyncStorage.setItem('max-months', '' + prefs.maxMonths);
}
if ('documentDir' in prefs) {
if (prefs.documentDir && (await fs.exists(prefs.documentDir))) {
await asyncStorage.setItem('document-dir', prefs.documentDir);
}
}
if ('floatingSidebar' in prefs) {
await asyncStorage.setItem('floating-sidebar', '' + prefs.floatingSidebar);
}
if ('language' in prefs) {
await asyncStorage.setItem('language', prefs.language);
}
if ('theme' in prefs) {
await asyncStorage.setItem('theme', prefs.theme);
}
if ('preferredDarkTheme' in prefs) {
await asyncStorage.setItem(
'preferred-dark-theme',
prefs.preferredDarkTheme,
);
}
if ('serverSelfSignedCert' in prefs) {
await asyncStorage.setItem(
'server-self-signed-cert',
prefs.serverSelfSignedCert,
);
}
return 'ok';
}

Comment on lines +61 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for async storage operations.

The function should handle potential storage errors to prevent uncaught exceptions from propagating.

 async function saveGlobalPrefs(prefs: GlobalPrefs) {
+  try {
     if ('maxMonths' in prefs) {
       await asyncStorage.setItem('max-months', '' + prefs.maxMonths);
     }
     if ('documentDir' in prefs) {
       if (prefs.documentDir && (await fs.exists(prefs.documentDir))) {
         await asyncStorage.setItem('document-dir', prefs.documentDir);
       }
     }
     if ('floatingSidebar' in prefs) {
       await asyncStorage.setItem('floating-sidebar', '' + prefs.floatingSidebar);
     }
     if ('language' in prefs) {
       await asyncStorage.setItem('language', prefs.language);
     }
     if ('theme' in prefs) {
       await asyncStorage.setItem('theme', prefs.theme);
     }
     if ('preferredDarkTheme' in prefs) {
       await asyncStorage.setItem(
         'preferred-dark-theme',
         prefs.preferredDarkTheme,
       );
     }
     if ('serverSelfSignedCert' in prefs) {
       await asyncStorage.setItem(
         'server-self-signed-cert',
         prefs.serverSelfSignedCert,
       );
     }
     return 'ok';
+  } catch (error) {
+    console.error('Failed to save global preferences:', error);
+    throw new Error(`Failed to save global preferences: ${error.message}`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function saveGlobalPrefs(prefs: GlobalPrefs) {
if ('maxMonths' in prefs) {
await asyncStorage.setItem('max-months', '' + prefs.maxMonths);
}
if ('documentDir' in prefs) {
if (prefs.documentDir && (await fs.exists(prefs.documentDir))) {
await asyncStorage.setItem('document-dir', prefs.documentDir);
}
}
if ('floatingSidebar' in prefs) {
await asyncStorage.setItem('floating-sidebar', '' + prefs.floatingSidebar);
}
if ('language' in prefs) {
await asyncStorage.setItem('language', prefs.language);
}
if ('theme' in prefs) {
await asyncStorage.setItem('theme', prefs.theme);
}
if ('preferredDarkTheme' in prefs) {
await asyncStorage.setItem(
'preferred-dark-theme',
prefs.preferredDarkTheme,
);
}
if ('serverSelfSignedCert' in prefs) {
await asyncStorage.setItem(
'server-self-signed-cert',
prefs.serverSelfSignedCert,
);
}
return 'ok';
}
async function saveGlobalPrefs(prefs: GlobalPrefs) {
try {
if ('maxMonths' in prefs) {
await asyncStorage.setItem('max-months', '' + prefs.maxMonths);
}
if ('documentDir' in prefs) {
if (prefs.documentDir && (await fs.exists(prefs.documentDir))) {
await asyncStorage.setItem('document-dir', prefs.documentDir);
}
}
if ('floatingSidebar' in prefs) {
await asyncStorage.setItem('floating-sidebar', '' + prefs.floatingSidebar);
}
if ('language' in prefs) {
await asyncStorage.setItem('language', prefs.language);
}
if ('theme' in prefs) {
await asyncStorage.setItem('theme', prefs.theme);
}
if ('preferredDarkTheme' in prefs) {
await asyncStorage.setItem(
'preferred-dark-theme',
prefs.preferredDarkTheme,
);
}
if ('serverSelfSignedCert' in prefs) {
await asyncStorage.setItem(
'server-self-signed-cert',
prefs.serverSelfSignedCert,
);
}
return 'ok';
} catch (error) {
console.error('Failed to save global preferences:', error);
throw new Error(`Failed to save global preferences: ${error.message}`);
}
}

async function loadGlobalPrefs() {
const [
[, floatingSidebar],
[, maxMonths],
[, documentDir],
[, encryptKey],
[, language],
[, theme],
[, preferredDarkTheme],
[, serverSelfSignedCert],
] = await asyncStorage.multiGet([
'floating-sidebar',
'max-months',
'document-dir',
'encrypt-key',
'language',
'theme',
'preferred-dark-theme',
'server-self-signed-cert',
] as const);
return {
floatingSidebar: floatingSidebar === 'true' ? true : false,
maxMonths: stringToInteger(maxMonths || ''),
documentDir: documentDir || getDefaultDocumentDir(),
keyId: encryptKey && JSON.parse(encryptKey).id,
language,
theme:
theme === 'light' ||
theme === 'dark' ||
theme === 'auto' ||
theme === 'development' ||
theme === 'midnight'
? theme
: 'auto',
preferredDarkTheme:
preferredDarkTheme === 'dark' || preferredDarkTheme === 'midnight'
? preferredDarkTheme
: 'dark',
serverSelfSignedCert: serverSelfSignedCert || undefined,
};
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for async storage operations.

The function should handle potential storage errors to prevent uncaught exceptions from propagating.

 async function loadGlobalPrefs() {
+  try {
     const [
       [, floatingSidebar],
       [, maxMonths],
       [, documentDir],
       [, encryptKey],
       [, language],
       [, theme],
       [, preferredDarkTheme],
       [, serverSelfSignedCert],
     ] = await asyncStorage.multiGet([
       'floating-sidebar',
       'max-months',
       'document-dir',
       'encrypt-key',
       'language',
       'theme',
       'preferred-dark-theme',
       'server-self-signed-cert',
     ] as const);
     return {
       floatingSidebar: floatingSidebar === 'true',
       maxMonths: stringToInteger(maxMonths || ''),
       documentDir: documentDir || getDefaultDocumentDir(),
       keyId: encryptKey && JSON.parse(encryptKey).id,
       language,
       theme:
         theme === 'light' ||
         theme === 'dark' ||
         theme === 'auto' ||
         theme === 'development' ||
         theme === 'midnight'
           ? theme
           : 'auto',
       preferredDarkTheme:
         preferredDarkTheme === 'dark' || preferredDarkTheme === 'midnight'
           ? preferredDarkTheme
           : 'dark',
       serverSelfSignedCert: serverSelfSignedCert || undefined,
     };
+  } catch (error) {
+    console.error('Failed to load global preferences:', error);
+    throw new Error(`Failed to load global preferences: ${error.message}`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function loadGlobalPrefs() {
const [
[, floatingSidebar],
[, maxMonths],
[, documentDir],
[, encryptKey],
[, language],
[, theme],
[, preferredDarkTheme],
[, serverSelfSignedCert],
] = await asyncStorage.multiGet([
'floating-sidebar',
'max-months',
'document-dir',
'encrypt-key',
'language',
'theme',
'preferred-dark-theme',
'server-self-signed-cert',
] as const);
return {
floatingSidebar: floatingSidebar === 'true' ? true : false,
maxMonths: stringToInteger(maxMonths || ''),
documentDir: documentDir || getDefaultDocumentDir(),
keyId: encryptKey && JSON.parse(encryptKey).id,
language,
theme:
theme === 'light' ||
theme === 'dark' ||
theme === 'auto' ||
theme === 'development' ||
theme === 'midnight'
? theme
: 'auto',
preferredDarkTheme:
preferredDarkTheme === 'dark' || preferredDarkTheme === 'midnight'
? preferredDarkTheme
: 'dark',
serverSelfSignedCert: serverSelfSignedCert || undefined,
};
}
async function loadGlobalPrefs() {
try {
const [
[, floatingSidebar],
[, maxMonths],
[, documentDir],
[, encryptKey],
[, language],
[, theme],
[, preferredDarkTheme],
[, serverSelfSignedCert],
] = await asyncStorage.multiGet([
'floating-sidebar',
'max-months',
'document-dir',
'encrypt-key',
'language',
'theme',
'preferred-dark-theme',
'server-self-signed-cert',
] as const);
return {
floatingSidebar: floatingSidebar === 'true',
maxMonths: stringToInteger(maxMonths || ''),
documentDir: documentDir || getDefaultDocumentDir(),
keyId: encryptKey && JSON.parse(encryptKey).id,
language,
theme:
theme === 'light' ||
theme === 'dark' ||
theme === 'auto' ||
theme === 'development' ||
theme === 'midnight'
? theme
: 'auto',
preferredDarkTheme:
preferredDarkTheme === 'dark' || preferredDarkTheme === 'midnight'
? preferredDarkTheme
: 'dark',
serverSelfSignedCert: serverSelfSignedCert || undefined,
};
} catch (error) {
console.error('Failed to load global preferences:', error);
throw new Error(`Failed to load global preferences: ${error.message}`);
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 115-115: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)

async function saveMetadataPrefs(prefsToSet: MetadataPrefs) {
const { cloudFileId } = _getMetadataPrefs();

// Need to sync the budget name on the server as well
if (prefsToSet.budgetName && cloudFileId) {
const userToken = await asyncStorage.getItem('user-token');

const syncServer = getServer()?.SYNC_SERVER;
if (!syncServer) {
throw new Error('No sync server set');
}

await post(syncServer + '/update-user-filename', {
token: userToken,
fileId: cloudFileId,
name: prefsToSet.budgetName,
});
}

await _saveMetadataPrefs(prefsToSet);
return 'ok';
}

Comment on lines +136 to +157
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for network operations.

The function should handle potential network errors during server communication.

 async function saveMetadataPrefs(prefsToSet: MetadataPrefs) {
+  try {
     const { cloudFileId } = _getMetadataPrefs();

     // Need to sync the budget name on the server as well
     if (prefsToSet.budgetName && cloudFileId) {
       const userToken = await asyncStorage.getItem('user-token');

       const syncServer = getServer()?.SYNC_SERVER;
       if (!syncServer) {
         throw new Error('No sync server set');
       }

       await post(syncServer + '/update-user-filename', {
         token: userToken,
         fileId: cloudFileId,
         name: prefsToSet.budgetName,
       });
     }

     await _saveMetadataPrefs(prefsToSet);
     return 'ok';
+  } catch (error) {
+    if (error.message === 'No sync server set') {
+      throw error;
+    }
+    console.error('Failed to save metadata preferences:', error);
+    throw new Error(`Failed to save metadata preferences: ${error.message}`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function saveMetadataPrefs(prefsToSet: MetadataPrefs) {
const { cloudFileId } = _getMetadataPrefs();
// Need to sync the budget name on the server as well
if (prefsToSet.budgetName && cloudFileId) {
const userToken = await asyncStorage.getItem('user-token');
const syncServer = getServer()?.SYNC_SERVER;
if (!syncServer) {
throw new Error('No sync server set');
}
await post(syncServer + '/update-user-filename', {
token: userToken,
fileId: cloudFileId,
name: prefsToSet.budgetName,
});
}
await _saveMetadataPrefs(prefsToSet);
return 'ok';
}
async function saveMetadataPrefs(prefsToSet: MetadataPrefs) {
try {
const { cloudFileId } = _getMetadataPrefs();
// Need to sync the budget name on the server as well
if (prefsToSet.budgetName && cloudFileId) {
const userToken = await asyncStorage.getItem('user-token');
const syncServer = getServer()?.SYNC_SERVER;
if (!syncServer) {
throw new Error('No sync server set');
}
await post(syncServer + '/update-user-filename', {
token: userToken,
fileId: cloudFileId,
name: prefsToSet.budgetName,
});
}
await _saveMetadataPrefs(prefsToSet);
return 'ok';
} catch (error) {
if (error.message === 'No sync server set') {
throw error;
}
console.error('Failed to save metadata preferences:', error);
throw new Error(`Failed to save metadata preferences: ${error.message}`);
}
}

app.method('preferences/save', mutator(undoable(savePreferences)));
app.method('preferences/get', getPreferences);
async function loadMetadataPrefs(): Promise<MetadataPrefs> {
return _getMetadataPrefs();
}
10 changes: 0 additions & 10 deletions packages/loot-core/src/server/preferences/types/handlers.d.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/loot-core/src/types/handlers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { BudgetHandlers } from '../server/budget/types/handlers';
import type { DashboardHandlers } from '../server/dashboard/types/handlers';
import type { FiltersHandlers } from '../server/filters/types/handlers';
import type { NotesHandlers } from '../server/notes/types/handlers';
import type { PreferencesHandlers } from '../server/preferences/types/handlers';
import type { PreferencesHandlers } from '../server/preferences/app';
import type { ReportsHandlers } from '../server/reports/types/handlers';
import type { RulesHandlers } from '../server/rules/types/handlers';
import type { SchedulesHandlers } from '../server/schedules/types/handlers';
Expand Down
9 changes: 0 additions & 9 deletions packages/loot-core/src/types/server-handlers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
PayeeEntity,
} from './models';
import { OpenIdConfig } from './models/openid';
import { GlobalPrefs, MetadataPrefs } from './prefs';
// eslint-disable-next-line import/no-unresolved
import { Query } from './query';
import { EmptyObject } from './util';
Expand Down Expand Up @@ -118,14 +117,6 @@ export interface ServerHandlers {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
query: (query: Query) => Promise<{ data: any; dependencies: string[] }>;

'save-global-prefs': (prefs) => Promise<'ok'>;

'load-global-prefs': () => Promise<GlobalPrefs>;

'save-prefs': (prefsToSet) => Promise<'ok'>;

'load-prefs': () => Promise<MetadataPrefs | null>;

'sync-reset': () => Promise<{ error?: { reason: string; meta?: unknown } }>;

'sync-repair': () => Promise<unknown>;
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/4420.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [joel-jeremy]
---

Extract preferences related server handlers from main.ts to server/preferences/app.ts
Loading