From 26d0bda8b29f881c6be25159d01a1bfad2a6fedc Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Sun, 30 Jul 2023 04:07:49 +0800 Subject: [PATCH] chore: add more concrete types to `loot-core` (#1186) --- .../src/components/settings/Experimental.tsx | 2 +- .../src/hooks/useFeatureFlag.ts | 2 +- packages/desktop-client/src/style/theme.tsx | 2 +- .../src/client/state-types/prefs.d.ts | 60 +------- packages/loot-core/src/mocks/budget.ts | 11 +- .../src/server/accounts/parse-file.ts | 2 +- packages/loot-core/src/server/api.ts | 20 +-- .../loot-core/src/server/budget/actions.ts | 143 +++++++++++++++--- packages/loot-core/src/server/main.test.ts | 2 +- packages/loot-core/src/server/main.ts | 2 +- packages/loot-core/src/server/prefs.ts | 37 +++-- .../loot-core/src/server/server-config.ts | 16 +- packages/loot-core/src/server/sheet.ts | 41 +++-- .../src/server/spreadsheet/globals.ts | 2 +- .../src/server/spreadsheet/spreadsheet.ts | 68 ++++++--- .../loot-core/src/server/spreadsheet/util.ts | 2 +- packages/loot-core/src/server/sync/encoder.ts | 2 +- packages/loot-core/src/server/sync/index.ts | 3 +- packages/loot-core/src/shared/months.ts | 72 ++++----- packages/loot-core/src/shared/util.ts | 2 +- packages/loot-core/src/types/prefs.d.ts | 53 +++++++ upcoming-release-notes/1186.md | 6 + 22 files changed, 352 insertions(+), 198 deletions(-) create mode 100644 packages/loot-core/src/types/prefs.d.ts create mode 100644 upcoming-release-notes/1186.md diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx index e2478e95d..d0fe85562 100644 --- a/packages/desktop-client/src/components/settings/Experimental.tsx +++ b/packages/desktop-client/src/components/settings/Experimental.tsx @@ -1,7 +1,7 @@ import { type ReactNode, useState } from 'react'; import { useSelector } from 'react-redux'; -import type { FeatureFlag } from 'loot-core/src/client/state-types/prefs'; +import type { FeatureFlag } from 'loot-core/src/types/prefs'; import { useActions } from '../../hooks/useActions'; import useFeatureFlag from '../../hooks/useFeatureFlag'; diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts index 6d47a634e..fcb97731b 100644 --- a/packages/desktop-client/src/hooks/useFeatureFlag.ts +++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts @@ -1,6 +1,6 @@ import { useSelector } from 'react-redux'; -import { type FeatureFlag } from 'loot-core/src/client/state-types/prefs'; +import type { FeatureFlag } from 'loot-core/src/types/prefs'; const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = { reportBudget: false, diff --git a/packages/desktop-client/src/style/theme.tsx b/packages/desktop-client/src/style/theme.tsx index 6f349bee1..b87fc90fd 100644 --- a/packages/desktop-client/src/style/theme.tsx +++ b/packages/desktop-client/src/style/theme.tsx @@ -1,6 +1,6 @@ import { useSelector } from 'react-redux'; -import type { Theme } from 'loot-core/src/client/state-types/prefs'; +import type { Theme } from 'loot-core/src/types/prefs'; import * as darkTheme from './themes/dark'; import * as lightTheme from './themes/light'; diff --git a/packages/loot-core/src/client/state-types/prefs.d.ts b/packages/loot-core/src/client/state-types/prefs.d.ts index 7611242dc..21ad02f3d 100644 --- a/packages/loot-core/src/client/state-types/prefs.d.ts +++ b/packages/loot-core/src/client/state-types/prefs.d.ts @@ -1,60 +1,6 @@ -import { type numberFormats } from '../../shared/util'; +import type { LocalPrefs, GlobalPrefs } from '../../types/prefs'; import type * as constants from '../constants'; -export type FeatureFlag = - | 'reportBudget' - | 'goalTemplatesEnabled' - | 'privacyMode' - | 'themes'; - -type NullableValues<T> = { [K in keyof T]: T[K] | null }; - -export type LocalPrefs = NullableValues< - { - firstDayOfWeekIdx: `${0 | 1 | 2 | 3 | 4 | 5 | 6}`; - dateFormat: string; - numberFormat: (typeof numberFormats)[number]['value']; - hideFraction: boolean; - hideClosedAccounts: boolean; - hideMobileMessage: boolean; - isPrivacyEnabled: boolean; - budgetName: string; - 'ui.showClosedAccounts': boolean; - 'expand-splits': boolean; - [key: `show-extra-balances-${string}`]: boolean; - [key: `hide-cleared-${string}`]: boolean; - 'budget.collapsed': boolean; - 'budget.summaryCollapsed': boolean; - 'budget.showHiddenCategories': boolean; - // TODO: pull from src/components/modals/ImportTransactions.js - [key: `parse-date-${string}-${'csv' | 'qif'}`]: string; - [key: `csv-mappings-${string}`]: string; - [key: `csv-delimiter-${string}`]: ',' | ';' | '\t'; - [key: `csv-has-header-${string}`]: boolean; - [key: `flip-amount-${string}-${'csv' | 'qif'}`]: boolean; - 'flags.updateNotificationShownForVersion': string; - id: string; - isCached: boolean; - lastUploaded: string; - cloudFileId: string; - groupId: string; - budgetType: 'report' | 'rollover'; - encryptKeyId: string; - lastSyncedTimestamp: string; - userId: string; - resetClock: boolean; - lastScheduleRun: string; - } & Record<`flags.${FeatureFlag}`, boolean> ->; - -export type Theme = 'light' | 'dark'; -export type GlobalPrefs = NullableValues<{ - floatingSidebar: boolean; - maxMonths: number; - theme: Theme; - documentDir: string; // Electron only -}>; - export type PrefsState = { local: LocalPrefs | null; global: GlobalPrefs | null; @@ -68,12 +14,12 @@ export type SetPrefsAction = { export type MergeLocalPrefsAction = { type: typeof constants.MERGE_LOCAL_PREFS; - prefs: Partial<LocalPrefs>; + prefs: LocalPrefs; }; export type MergeGlobalPrefsAction = { type: typeof constants.MERGE_GLOBAL_PREFS; - globalPrefs: Partial<GlobalPrefs>; + globalPrefs: GlobalPrefs; }; export type PrefsActions = diff --git a/packages/loot-core/src/mocks/budget.ts b/packages/loot-core/src/mocks/budget.ts index ccfb3e608..2d592cf7b 100644 --- a/packages/loot-core/src/mocks/budget.ts +++ b/packages/loot-core/src/mocks/budget.ts @@ -453,10 +453,10 @@ async function createBudget(accounts, payees, groups) { } function setBudgetIfSpent(month, cat) { - let spent = sheet.getCellValue( + let spent: number = sheet.getCellValue( monthUtils.sheetForMonth(month), `sum-amount-${cat.id}`, - ); + ) as number; if (spent < 0) { setBudget(month, cat, -spent); @@ -514,7 +514,10 @@ async function createBudget(accounts, payees, groups) { month <= monthUtils.currentMonth() ) { let sheetName = monthUtils.sheetForMonth(month); - let toBudget = sheet.getCellValue(sheetName, 'to-budget'); + let toBudget: number = sheet.getCellValue( + sheetName, + 'to-budget', + ) as number; let available = toBudget - prevSaved; if (available - 403000 > 0) { @@ -533,7 +536,7 @@ async function createBudget(accounts, payees, groups) { await sheet.waitOnSpreadsheet(); let sheetName = monthUtils.sheetForMonth(monthUtils.currentMonth()); - let toBudget = sheet.getCellValue(sheetName, 'to-budget'); + let toBudget: number = sheet.getCellValue(sheetName, 'to-budget') as number; if (toBudget < 0) { await addTransactions(primaryAccount.id, [ { diff --git a/packages/loot-core/src/server/accounts/parse-file.ts b/packages/loot-core/src/server/accounts/parse-file.ts index ea6384083..16434a8a2 100644 --- a/packages/loot-core/src/server/accounts/parse-file.ts +++ b/packages/loot-core/src/server/accounts/parse-file.ts @@ -130,7 +130,7 @@ async function parseOFX(filepath): Promise<ParseFileResult> { transactions: data.map(trans => ({ amount: trans.amount, imported_id: trans.fi_id, - date: trans.date ? dayFromDate(trans.date * 1000) : null, + date: trans.date ? dayFromDate(new Date(trans.date * 1000)) : null, payee_name: useName ? trans.name : trans.memo, imported_payee: useName ? trans.name : trans.memo, notes: useName ? trans.memo || null : null, //memo used for payee diff --git a/packages/loot-core/src/server/api.ts b/packages/loot-core/src/server/api.ts index 885d21fc0..01190365d 100644 --- a/packages/loot-core/src/server/api.ts +++ b/packages/loot-core/src/server/api.ts @@ -299,16 +299,16 @@ handlers['api/budget-month'] = async function ({ month }) { // different (for now) return { month, - incomeAvailable: value('available-funds'), - lastMonthOverspent: value('last-month-overspent'), - forNextMonth: value('buffered'), - totalBudgeted: value('total-budgeted'), - toBudget: value('to-budget'), - - fromLastMonth: value('from-last-month'), - totalIncome: value('total-income'), - totalSpent: value('total-spent'), - totalBalance: value('total-leftover'), + incomeAvailable: value('available-funds') as number, + lastMonthOverspent: value('last-month-overspent') as number, + forNextMonth: value('buffered') as number, + totalBudgeted: value('total-budgeted') as number, + toBudget: value('to-budget') as number, + + fromLastMonth: value('from-last-month') as number, + totalIncome: value('total-income') as number, + totalSpent: value('total-spent') as number, + totalBalance: value('total-leftover') as number, categoryGroups: groups.map(group => { if (group.is_income) { diff --git a/packages/loot-core/src/server/budget/actions.ts b/packages/loot-core/src/server/budget/actions.ts index 0dd5a735b..62caada23 100644 --- a/packages/loot-core/src/server/budget/actions.ts +++ b/packages/loot-core/src/server/budget/actions.ts @@ -5,7 +5,10 @@ import * as prefs from '../prefs'; import * as sheet from '../sheet'; import { batchMessages } from '../sync'; -export async function getSheetValue(sheetName, cell) { +export async function getSheetValue( + sheetName: string, + cell: string, +): Promise<number> { const node = await sheet.getCell(sheetName, cell); return safeNumber(typeof node.value === 'number' ? node.value : 0); } @@ -14,26 +17,37 @@ export async function getSheetValue(sheetName, cell) { // forth. buffered should never be allowed to go into the negative, // and you shouldn't be allowed to pull non-existant money from // leftover. -function calcBufferedAmount(toBudget, buffered, amount) { +function calcBufferedAmount( + toBudget: number, + buffered: number, + amount: number, +): number { amount = Math.min(Math.max(amount, -buffered), Math.max(toBudget, 0)); return buffered + amount; } -function getBudgetTable() { +function getBudgetTable(): string { let { budgetType } = prefs.getPrefs() || {}; return budgetType === 'report' ? 'reflect_budgets' : 'zero_budgets'; } -export function isReflectBudget() { +export function isReflectBudget(): boolean { let { budgetType } = prefs.getPrefs(); return budgetType === 'report'; } -function dbMonth(month) { +function dbMonth(month: string): number { return parseInt(month.replace('-', '')); } -function getBudgetData(table, month) { +// TODO: complete list of fields. +type BudgetData = { + is_income: 1 | 0; + category: string; + amount: number; +}; + +function getBudgetData(table: string, month: string): Promise<BudgetData[]> { return db.all( ` SELECT b.*, c.is_income FROM v_categories c @@ -44,7 +58,7 @@ function getBudgetData(table, month) { ); } -function getAllMonths(startMonth) { +function getAllMonths(startMonth: string): string[] { let { createdMonths } = sheet.get().meta(); let latest = null; for (let month of createdMonths) { @@ -57,7 +71,13 @@ function getAllMonths(startMonth) { // TODO: Valid month format in all the functions below -export function getBudget({ category, month }) { +export function getBudget({ + category, + month, +}: { + category: string; + month: string; +}): number { let table = getBudgetTable(); let existing = db.firstSync( `SELECT * FROM ${table} WHERE month = ? AND category = ?`, @@ -66,7 +86,15 @@ export function getBudget({ category, month }) { return existing ? existing.amount || 0 : 0; } -export function setBudget({ category, month, amount }) { +export function setBudget({ + category, + month, + amount, +}: { + category: string; + month: string; + amount: unknown; +}): Promise<void> { amount = safeNumber(typeof amount === 'number' ? amount : 0); const table = getBudgetTable(); @@ -85,7 +113,7 @@ export function setBudget({ category, month, amount }) { }); } -export function setBuffer(month, amount) { +export function setBuffer(month: string, amount: unknown): Promise<void> { let existing = db.firstSync( `SELECT id FROM zero_budget_months WHERE id = ?`, [month], @@ -99,7 +127,12 @@ export function setBuffer(month, amount) { return db.insert('zero_budget_months', { id: month, buffered: amount }); } -function setCarryover(table, category, month, flag) { +function setCarryover( + table: string, + category: string, + month: string, + flag: boolean, +): Promise<void> { let existing = db.firstSync( `SELECT id FROM ${table} WHERE month = ? AND category = ?`, [month, category], @@ -117,10 +150,14 @@ function setCarryover(table, category, month, flag) { // Actions -export async function copyPreviousMonth({ month }) { +export async function copyPreviousMonth({ + month, +}: { + month: string; +}): Promise<void> { let prevMonth = dbMonth(monthUtils.prevMonth(month)); let table = getBudgetTable(); - let budgetData = await getBudgetData(table, prevMonth); + let budgetData = await getBudgetData(table, prevMonth.toString()); await batchMessages(async () => { budgetData.forEach(prevBudget => { @@ -136,7 +173,13 @@ export async function copyPreviousMonth({ month }) { }); } -export async function copySinglePreviousMonth({ month, category }) { +export async function copySinglePreviousMonth({ + month, + category, +}: { + month: string; + category: string; +}): Promise<void> { let prevMonth = monthUtils.prevMonth(month); let newAmount = await getSheetValue( monthUtils.sheetForMonth(prevMonth), @@ -147,7 +190,7 @@ export async function copySinglePreviousMonth({ month, category }) { }); } -export async function setZero({ month }) { +export async function setZero({ month }: { month: string }): Promise<void> { let categories = await db.all( 'SELECT * FROM v_categories WHERE tombstone = 0', ); @@ -162,7 +205,11 @@ export async function setZero({ month }) { }); } -export async function set3MonthAvg({ month }) { +export async function set3MonthAvg({ + month, +}: { + month: string; +}): Promise<void> { let categories = await db.all( 'SELECT * FROM v_categories WHERE tombstone = 0', ); @@ -196,7 +243,15 @@ export async function set3MonthAvg({ month }) { }); } -export async function setNMonthAvg({ month, N, category }) { +export async function setNMonthAvg({ + month, + N, + category, +}: { + month: string; + N: number; + category: string; +}): Promise<void> { let prevMonth = monthUtils.prevMonth(month); let sumAmount = 0; for (let l = 0; l < N; l++) { @@ -212,7 +267,13 @@ export async function setNMonthAvg({ month, N, category }) { }); } -export async function holdForNextMonth({ month, amount }) { +export async function holdForNextMonth({ + month, + amount, +}: { + month: string; + amount: number; +}): Promise<boolean> { let row = await db.first( 'SELECT buffered FROM zero_budget_months WHERE id = ?', [month], @@ -234,11 +295,19 @@ export async function holdForNextMonth({ month, amount }) { return false; } -export async function resetHold({ month }) { +export async function resetHold({ month }: { month: string }): Promise<void> { await setBuffer(month, 0); } -export async function coverOverspending({ month, to, from }) { +export async function coverOverspending({ + month, + to, + from, +}: { + month: string; + to: string; + from: string; +}): Promise<void> { let sheetName = monthUtils.sheetForMonth(month); let toBudgeted = await getSheetValue(sheetName, 'budget-' + to); let leftover = await getSheetValue(sheetName, 'leftover-' + to); @@ -266,7 +335,15 @@ export async function coverOverspending({ month, to, from }) { await setBudget({ category: to, month, amount: toBudgeted + amountCovered }); } -export async function transferAvailable({ month, amount, category }) { +export async function transferAvailable({ + month, + amount, + category, +}: { + month: string; + amount: number; + category: string; +}): Promise<void> { let sheetName = monthUtils.sheetForMonth(month); let leftover = await getSheetValue(sheetName, 'to-budget'); amount = Math.max(Math.min(amount, leftover), 0); @@ -275,7 +352,17 @@ export async function transferAvailable({ month, amount, category }) { await setBudget({ category, month, amount: budgeted + amount }); } -export async function transferCategory({ month, amount, from, to }) { +export async function transferCategory({ + month, + amount, + from, + to, +}: { + month: string; + amount: number; + to: string; + from: string; +}): Promise<void> { const sheetName = monthUtils.sheetForMonth(month); const fromBudgeted = await getSheetValue(sheetName, 'budget-' + from); @@ -289,13 +376,21 @@ export async function transferCategory({ month, amount, from, to }) { } } -export async function setCategoryCarryover({ startMonth, category, flag }) { +export async function setCategoryCarryover({ + startMonth, + category, + flag, +}: { + startMonth: string; + category: string; + flag: boolean; +}): Promise<void> { let table = getBudgetTable(); let months = getAllMonths(startMonth); await batchMessages(async () => { for (let month of months) { - setCarryover(table, category, dbMonth(month), flag); + setCarryover(table, category, dbMonth(month).toString(), flag); } }); } diff --git a/packages/loot-core/src/server/main.test.ts b/packages/loot-core/src/server/main.test.ts index bcb709f09..8854ae574 100644 --- a/packages/loot-core/src/server/main.test.ts +++ b/packages/loot-core/src/server/main.test.ts @@ -101,7 +101,7 @@ describe('Budgets', () => { describe('Accounts', () => { test('create accounts with correct starting balance', async () => { prefs.loadPrefs(); - prefs.savePrefs({ clientId: 'client', groupId: 'group' }); + prefs.savePrefs({ groupId: 'group' }); await runMutator(async () => { // An income category is required because the starting balance is diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index a46ad4e9a..adcffb081 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -263,7 +263,7 @@ handlers['report-budget-month'] = async function ({ month }) { }; handlers['budget-set-type'] = async function ({ type }) { - if (type !== 'rollover' && type !== 'report') { + if (!prefs.BUDGET_TYPES.includes(type)) { throw new Error('Invalid budget type: ' + type); } diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index 46ced65dd..0cb45bcdc 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -1,14 +1,18 @@ import { Timestamp } from '@actual-app/crdt'; import * as fs from '../platform/server/fs'; +import type { LocalPrefs } from '../types/prefs'; -import { sendMessages } from './sync'; +import { Message, sendMessages } from './sync'; -let prefs = null; +export const BUDGET_TYPES = ['report', 'rollover'] as const; +export type BudgetType = (typeof BUDGET_TYPES)[number]; -export async function loadPrefs(id?) { +let prefs: LocalPrefs = null; + +export async function loadPrefs(id?: string): Promise<LocalPrefs> { if (process.env.NODE_ENV === 'test' && !id) { - prefs = { dummyTestPrefs: true }; + prefs = getDefaultPrefs('test', 'test_LocalPrefs'); return prefs; } @@ -42,12 +46,15 @@ export async function loadPrefs(id?) { return prefs; } -export async function savePrefs(prefsToSet, { avoidSync = false } = {}) { +export async function savePrefs( + prefsToSet: LocalPrefs, + { avoidSync = false } = {}, +): Promise<void> { Object.assign(prefs, prefsToSet); if (!avoidSync) { // Sync whitelisted prefs - let messages = Object.keys(prefsToSet) + const messages: Message[] = Object.keys(prefsToSet) .map(key => { if (key === 'budgetType' || key === 'budgetName') { return { @@ -67,30 +74,20 @@ export async function savePrefs(prefsToSet, { avoidSync = false } = {}) { } } - if (!prefs.dummyTestPrefs) { + if (process.env.NODE_ENV !== 'test') { let prefsPath = fs.join(fs.getBudgetDir(prefs.id), 'metadata.json'); await fs.writeFile(prefsPath, JSON.stringify(prefs)); } } -export function unloadPrefs() { +export function unloadPrefs(): void { prefs = null; } -export function getPrefs() { +export function getPrefs(): LocalPrefs { return prefs; } -export function getDefaultPrefs(id, budgetName) { +export function getDefaultPrefs(id: string, budgetName: string): LocalPrefs { return { id, budgetName }; } - -export async function readPrefs(id) { - const fullpath = fs.join(fs.getBudgetDir(id), 'metadata.json'); - - try { - return JSON.parse(await fs.readFile(fullpath)); - } catch (e) { - return null; - } -} diff --git a/packages/loot-core/src/server/server-config.ts b/packages/loot-core/src/server/server-config.ts index 2bf09ec5e..623cced37 100644 --- a/packages/loot-core/src/server/server-config.ts +++ b/packages/loot-core/src/server/server-config.ts @@ -1,14 +1,22 @@ import * as fs from '../platform/server/fs'; -let config = null; +type ServerConfig = { + BASE_SERVER: string; + SYNC_SERVER: string; + SIGNUP_SERVER: string; + PLAID_SERVER: string; + GOCARDLESS_SERVER: string; +}; -function joinURL(base, ...paths) { +let config: ServerConfig | null = null; + +function joinURL(base: string | URL, ...paths: string[]): string { let url = new URL(base); url.pathname = fs.join(...paths); return url.toString(); } -export function setServer(url) { +export function setServer(url: string): void { if (url == null) { config = null; } else { @@ -17,7 +25,7 @@ export function setServer(url) { } // `url` is optional; if not given it will provide the global config -export function getServer(url?) { +export function getServer(url?: string): ServerConfig | null { if (url) { return { BASE_SERVER: url, diff --git a/packages/loot-core/src/server/sheet.ts b/packages/loot-core/src/server/sheet.ts index 8eb5341c0..4c67f935e 100644 --- a/packages/loot-core/src/server/sheet.ts +++ b/packages/loot-core/src/server/sheet.ts @@ -1,3 +1,5 @@ +import { type Database } from 'better-sqlite3'; + import { captureBreadcrumb } from '../platform/exceptions'; import * as sqlite from '../platform/server/sqlite'; import { sheetForMonth } from '../shared/months'; @@ -7,14 +9,15 @@ import * as prefs from './prefs'; import Spreadsheet from './spreadsheet/spreadsheet'; import { resolveName } from './spreadsheet/util'; -let globalSheet, globalOnChange; +let globalSheet: Spreadsheet; +let globalOnChange; let globalCacheDb; -export function get() { +export function get(): Spreadsheet { return globalSheet; } -async function updateSpreadsheetCache(rawDb, names) { +async function updateSpreadsheetCache(rawDb, names: string[]) { await sqlite.transaction(rawDb, () => { names.forEach(name => { const node = globalSheet._getNode(name); @@ -31,7 +34,11 @@ async function updateSpreadsheetCache(rawDb, names) { }); } -function setCacheStatus(mainDb, cacheDb, { clean }) { +function setCacheStatus( + mainDb: unknown, + cacheDb: unknown, + { clean }: { clean: boolean }, +) { if (clean) { // Generate random number and stick in both places let num = Math.random() * 10000000; @@ -53,7 +60,7 @@ function setCacheStatus(mainDb, cacheDb, { clean }) { } } -function isCacheDirty(mainDb, cacheDb) { +function isCacheDirty(mainDb: Database, cacheDb: Database): boolean { let rows = sqlite.runQuery<{ key?: number }>( cacheDb, 'SELECT key FROM kvcache_key WHERE id = 1', @@ -84,7 +91,10 @@ function isCacheDirty(mainDb, cacheDb) { return rows.length === 0; } -export async function loadSpreadsheet(db, onSheetChange?) { +export async function loadSpreadsheet( + db, + onSheetChange?, +): Promise<Spreadsheet> { let cacheEnabled = process.env.NODE_ENV !== 'test'; let mainDb = db.getDatabase(); let cacheDb; @@ -157,7 +167,7 @@ export async function loadSpreadsheet(db, onSheetChange?) { return sheet; } -export function unloadSpreadsheet() { +export function unloadSpreadsheet(): void { if (globalSheet) { // TODO: Should wait for the sheet to finish globalSheet.unload(); @@ -170,14 +180,14 @@ export function unloadSpreadsheet() { } } -export async function reloadSpreadsheet(db) { +export async function reloadSpreadsheet(db): Promise<Spreadsheet> { if (globalSheet) { unloadSpreadsheet(); return loadSpreadsheet(db, globalOnChange); } } -export async function loadUserBudgets(db) { +export async function loadUserBudgets(db): Promise<void> { let sheet = globalSheet; // TODO: Clear out the cache here so make sure future loads of the app @@ -218,27 +228,30 @@ export async function loadUserBudgets(db) { sheet.endTransaction(); } -export function getCell(sheet, name) { +export function getCell(sheet: string, name: string) { return globalSheet._getNode(resolveName(sheet, name)); } -export function getCellValue(sheet, name) { +export function getCellValue( + sheet: string, + name: string, +): string | number | boolean { return globalSheet.getValue(resolveName(sheet, name)); } -export function startTransaction() { +export function startTransaction(): void { if (globalSheet) { globalSheet.startTransaction(); } } -export function endTransaction() { +export function endTransaction(): void { if (globalSheet) { globalSheet.endTransaction(); } } -export function waitOnSpreadsheet() { +export function waitOnSpreadsheet(): Promise<void> { return new Promise(resolve => { if (globalSheet) { globalSheet.onFinish(resolve); diff --git a/packages/loot-core/src/server/spreadsheet/globals.ts b/packages/loot-core/src/server/spreadsheet/globals.ts index 45b656409..8bc525674 100644 --- a/packages/loot-core/src/server/spreadsheet/globals.ts +++ b/packages/loot-core/src/server/spreadsheet/globals.ts @@ -1,4 +1,4 @@ -export function number(v) { +export function number(v: unknown): number { if (typeof v === 'number') { return v; } else if (typeof v === 'string') { diff --git a/packages/loot-core/src/server/spreadsheet/spreadsheet.ts b/packages/loot-core/src/server/spreadsheet/spreadsheet.ts index cab861106..53f62431d 100644 --- a/packages/loot-core/src/server/spreadsheet/spreadsheet.ts +++ b/packages/loot-core/src/server/spreadsheet/spreadsheet.ts @@ -5,6 +5,18 @@ import { compileQuery, runCompiledQuery, schema, schemaConfig } from '../aql'; import Graph from './graph-data-structure'; import { unresolveName, resolveName } from './util'; +type Node = { + name: string; + expr: string | number | boolean; + value: string | number | boolean; + sheet: unknown; + query?: string; + sql?: { sqlPieces: unknown; state: { dependencies: unknown[] } }; + dynamic?: boolean; + _run?: unknown; + _dependencies?: string[]; +}; + export default class Spreadsheet { _meta; cacheBarrier; @@ -12,7 +24,7 @@ export default class Spreadsheet { dirtyCells; events; graph; - nodes; + nodes: Map<string, Node>; running; saveCache; setCacheStatus; @@ -21,7 +33,7 @@ export default class Spreadsheet { constructor(saveCache?: unknown, setCacheStatus?: unknown) { // @ts-expect-error Graph should be converted to class this.graph = new Graph(); - this.nodes = new Map(); + this.nodes = new Map<string, Node>(); this.transactionDepth = 0; this.saveCache = saveCache; this.setCacheStatus = setCacheStatus; @@ -43,7 +55,7 @@ export default class Spreadsheet { // Spreadsheet interface - _getNode(name) { + _getNode(name: string): Node { const { sheet } = unresolveName(name); if (!this.nodes.has(name)) { @@ -293,13 +305,13 @@ export default class Spreadsheet { }); } - load(name, value) { + load(name: string, value: string | number | boolean): void { const node = this._getNode(name); node.expr = value; node.value = value; } - create(name, value) { + create(name: string, value: string | number | boolean) { return this.transaction(() => { const node = this._getNode(name); node.expr = value; @@ -308,24 +320,24 @@ export default class Spreadsheet { }); } - set(name, value) { + set(name: string, value: string | number | boolean): void { this.create(name, value); } - recompute(name) { + recompute(name: string): void { this.transaction(() => { this.dirtyCells.push(name); }); } - recomputeAll() { + recomputeAll(): void { // Recompute everything! this.transaction(() => { this.dirtyCells = [...this.nodes.keys()]; }); } - createQuery(sheetName, cellName, query) { + createQuery(sheetName: string, cellName: string, query: string): void { let name = resolveName(sheetName, cellName); let node = this._getNode(name); @@ -340,7 +352,11 @@ export default class Spreadsheet { } } - createStatic(sheetName, cellName, initialValue) { + createStatic( + sheetName: string, + cellName: string, + initialValue: number | boolean, + ): void { let name = resolveName(sheetName, cellName); let exists = this.nodes.has(name); if (!exists) { @@ -349,10 +365,20 @@ export default class Spreadsheet { } createDynamic( - sheetName, - cellName, - { dependencies = [], run, initialValue, refresh = false }, - ) { + sheetName: string, + cellName: string, + { + dependencies = [], + run, + initialValue, + refresh = false, + }: { + dependencies?: string[]; + run?: unknown; + initialValue: number | boolean; + refresh?: boolean; + }, + ): void { let name = resolveName(sheetName, cellName); let node = this._getNode(name); @@ -391,7 +417,7 @@ export default class Spreadsheet { } } - clearSheet(sheetName) { + clearSheet(sheetName: string): void { for (let [name, node] of this.nodes.entries()) { if (node.sheet === sheetName) { this.nodes.delete(name); @@ -399,19 +425,19 @@ export default class Spreadsheet { } } - voidCell(sheetName, name, voidValue = null) { + voidCell(sheetName: string, name: string, voidValue = null): void { let node = this.getNode(resolveName(sheetName, name)); node._run = null; node.dynamic = false; node.value = voidValue; } - deleteCell(sheetName, name) { + deleteCell(sheetName: string, name: string): void { this.voidCell(sheetName, name); this.nodes.delete(resolveName(sheetName, name)); } - addDependencies(sheetName, cellName, deps) { + addDependencies(sheetName: string, cellName: string, deps: string[]): void { let name = resolveName(sheetName, cellName); deps = deps.map(dep => { @@ -435,7 +461,11 @@ export default class Spreadsheet { } } - removeDependencies(sheetName, cellName, deps) { + removeDependencies( + sheetName: string, + cellName: string, + deps: string[], + ): void { let name = resolveName(sheetName, cellName); deps = deps.map(dep => { diff --git a/packages/loot-core/src/server/spreadsheet/util.ts b/packages/loot-core/src/server/spreadsheet/util.ts index 92709cffa..001772ed3 100644 --- a/packages/loot-core/src/server/spreadsheet/util.ts +++ b/packages/loot-core/src/server/spreadsheet/util.ts @@ -9,6 +9,6 @@ export function unresolveName(name) { return { sheet: null, name }; } -export function resolveName(sheet, name) { +export function resolveName(sheet: string, name: string): string { return sheet + '!' + name; } diff --git a/packages/loot-core/src/server/sync/encoder.ts b/packages/loot-core/src/server/sync/encoder.ts index fc6df6c32..36aca1d5e 100644 --- a/packages/loot-core/src/server/sync/encoder.ts +++ b/packages/loot-core/src/server/sync/encoder.ts @@ -20,7 +20,7 @@ function coerceBuffer(value) { export async function encode( groupId: string, fileId: string, - since: Timestamp, + since: Timestamp | string, messages: Message[], ): Promise<Uint8Array> { let { encryptKeyId } = prefs.getPrefs(); diff --git a/packages/loot-core/src/server/sync/index.ts b/packages/loot-core/src/server/sync/index.ts index c3ec89329..34dceb7dd 100644 --- a/packages/loot-core/src/server/sync/index.ts +++ b/packages/loot-core/src/server/sync/index.ts @@ -12,6 +12,7 @@ import * as connection from '../../platform/server/connection'; import logger from '../../platform/server/log'; import { sequential, once } from '../../shared/async'; import { setIn, getIn } from '../../shared/util'; +import { LocalPrefs } from '../../types/prefs'; import { triggerBudgetChanges, setType as setBudgetType } from '../budget/base'; import * as db from '../db'; import { PostError, SyncError } from '../errors'; @@ -303,7 +304,7 @@ export const applyMessages = sequential(async (messages: Message[]) => { return data; } - let prefsToSet: Record<string, unknown> = {}; + let prefsToSet: LocalPrefs = {}; let oldData = await fetchData(); undo.appendMessages(messages, oldData); diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index 275f74965..df8248fe6 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -1,7 +1,9 @@ import * as d from 'date-fns'; import memoizeOne from 'memoize-one'; -export function _parse(value: string | number | Date) { +type DateLike = string | Date; + +export function _parse(value: DateLike): Date { if (typeof value === 'string') { // Dates are hard. We just want to deal with months in the format // 2020-01 and days in the format 2020-01-01, but life is never @@ -74,15 +76,15 @@ export function _parse(value: string | number | Date) { export const parseDate = _parse; -export function yearFromDate(date: string | number | Date) { +export function yearFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy'); } -export function monthFromDate(date: string | number | Date) { +export function monthFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy-MM'); } -export function dayFromDate(date: string | number | Date) { +export function dayFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy-MM-dd'); } @@ -94,7 +96,7 @@ export function currentMonth(): string { } } -export function currentDay() { +export function currentDay(): string { if (global.IS_TESTING) { return '2017-01-01'; } else { @@ -102,26 +104,26 @@ export function currentDay() { } } -export function nextMonth(month: string | Date) { +export function nextMonth(month: DateLike): string { return d.format(d.addMonths(_parse(month), 1), 'yyyy-MM'); } -export function prevMonth(month: string | Date) { +export function prevMonth(month: DateLike): string { return d.format(d.subMonths(_parse(month), 1), 'yyyy-MM'); } -export function addMonths(month: string | Date, n: number) { +export function addMonths(month: DateLike, n: number): string { return d.format(d.addMonths(_parse(month), n), 'yyyy-MM'); } -export function addWeeks(date: string | Date, n: number) { +export function addWeeks(date: DateLike, n: number): string { return d.format(d.addWeeks(_parse(date), n), 'yyyy-MM-dd'); } export function differenceInCalendarMonths( - month1: string | Date, - month2: string | Date, -) { + month1: DateLike, + month2: DateLike, +): number { return d.differenceInCalendarMonths(_parse(month1), _parse(month2)); } @@ -129,25 +131,25 @@ export function subMonths(month: string | Date, n: number) { return d.format(d.subMonths(_parse(month), n), 'yyyy-MM'); } -export function addDays(day: string | Date, n: number) { +export function addDays(day: DateLike, n: number): string { return d.format(d.addDays(_parse(day), n), 'yyyy-MM-dd'); } -export function subDays(day: string | Date, n: number) { +export function subDays(day: DateLike, n: number): string { return d.format(d.subDays(_parse(day), n), 'yyyy-MM-dd'); } -export function isBefore(month1: string | Date, month2: string | Date) { +export function isBefore(month1: DateLike, month2: DateLike): boolean { return d.isBefore(_parse(month1), _parse(month2)); } -export function isAfter(month1: string | Date, month2: string | Date) { +export function isAfter(month1: DateLike, month2: DateLike): boolean { return d.isAfter(_parse(month1), _parse(month2)); } // TODO: This doesn't really fit in this module anymore, should // probably live elsewhere -export function bounds(month: string | Date) { +export function bounds(month: DateLike): { start: number; end: number } { return { start: parseInt(d.format(d.startOfMonth(_parse(month)), 'yyyyMMdd')), end: parseInt(d.format(d.endOfMonth(_parse(month)), 'yyyyMMdd')), @@ -155,11 +157,11 @@ export function bounds(month: string | Date) { } export function _range( - start: string | Date, - end: string | Date, + start: DateLike, + end: DateLike, inclusive = false, ): string[] { - const months: string[] = []; + const months = []; let month = monthFromDate(start); while (d.isBefore(_parse(month), _parse(end))) { months.push(month); @@ -173,20 +175,20 @@ export function _range( return months; } -export function range(start: string, end: string) { +export function range(start: DateLike, end: DateLike): string[] { return _range(start, end); } -export function rangeInclusive(start: string, end: string) { +export function rangeInclusive(start: DateLike, end: DateLike): string[] { return _range(start, end, true); } export function _dayRange( - start: string, - end: string | Date, + start: DateLike, + end: DateLike, inclusive = false, ): string[] { - const days: string[] = []; + const days = []; let day = start; while (d.isBefore(_parse(day), _parse(end))) { days.push(day); @@ -200,44 +202,44 @@ export function _dayRange( return days; } -export function dayRange(start: string, end: string) { +export function dayRange(start: DateLike, end: DateLike) { return _dayRange(start, end); } -export function dayRangeInclusive(start: string, end: string) { +export function dayRangeInclusive(start: DateLike, end: DateLike) { return _dayRange(start, end, true); } -export function getMonthIndex(month: string) { +export function getMonthIndex(month: string): number { return parseInt(month.slice(5, 7)) - 1; } -export function getYear(month: string) { +export function getYear(month: string): string { return month.slice(0, 4); } -export function getMonth(day: string) { +export function getMonth(day: string): string { return day.slice(0, 7); } -export function getYearStart(month: string) { +export function getYearStart(month: string): string { return getYear(month) + '-01'; } -export function getYearEnd(month: string) { +export function getYearEnd(month: string): string { return getYear(month) + '-12'; } -export function sheetForMonth(month: string) { +export function sheetForMonth(month: string): string { return 'budget' + month.replace('-', ''); } -export function nameForMonth(month: string | Date) { +export function nameForMonth(month: DateLike): string { // eslint-disable-next-line rulesdir/typography return d.format(_parse(month), "MMMM 'yy"); } -export function format(month: string | Date, str: string) { +export function format(month: DateLike, str: string): string { return d.format(_parse(month), str); } diff --git a/packages/loot-core/src/shared/util.ts b/packages/loot-core/src/shared/util.ts index eedbceb87..22d5d60e2 100644 --- a/packages/loot-core/src/shared/util.ts +++ b/packages/loot-core/src/shared/util.ts @@ -262,7 +262,7 @@ setNumberFormat({ format: 'comma-dot', hideFraction: false }); const MAX_SAFE_NUMBER = 2 ** 51 - 1; const MIN_SAFE_NUMBER = -MAX_SAFE_NUMBER; -export function safeNumber(value) { +export function safeNumber(value: number) { if (!Number.isInteger(value)) { throw new Error( 'safeNumber: number is not an integer: ' + JSON.stringify(value), diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts new file mode 100644 index 000000000..4dcd19128 --- /dev/null +++ b/packages/loot-core/src/types/prefs.d.ts @@ -0,0 +1,53 @@ +import { type numberFormats } from '../shared/util'; + +export type FeatureFlag = + | 'reportBudget' + | 'goalTemplatesEnabled' + | 'privacyMode' + | 'themes'; + +export type LocalPrefs = Partial< + { + firstDayOfWeekIdx: `${0 | 1 | 2 | 3 | 4 | 5 | 6}`; + dateFormat: string; + numberFormat: (typeof numberFormats)[number]['value']; + hideFraction: boolean; + hideClosedAccounts: boolean; + hideMobileMessage: boolean; + isPrivacyEnabled: boolean; + budgetName: string; + 'ui.showClosedAccounts': boolean; + 'expand-splits': boolean; + [key: `show-extra-balances-${string}`]: boolean; + [key: `hide-cleared-${string}`]: boolean; + 'budget.collapsed': boolean; + 'budget.summaryCollapsed': boolean; + 'budget.showHiddenCategories': boolean; + // TODO: pull from src/components/modals/ImportTransactions.js + [key: `parse-date-${string}-${'csv' | 'qif'}`]: string; + [key: `csv-mappings-${string}`]: string; + [key: `csv-delimiter-${string}`]: ',' | ';' | '\t'; + [key: `csv-has-header-${string}`]: boolean; + [key: `flip-amount-${string}-${'csv' | 'qif'}`]: boolean; + 'flags.updateNotificationShownForVersion': string; + id: string; + isCached: boolean; + lastUploaded: string; + cloudFileId: string; + groupId: string; + budgetType: 'report' | 'rollover'; + encryptKeyId: string; + lastSyncedTimestamp: string; + userId: string; + resetClock: boolean; + lastScheduleRun: string; + } & Record<`flags.${FeatureFlag}`, boolean> +>; + +export type Theme = 'light' | 'dark'; +export type GlobalPrefs = Partial<{ + floatingSidebar: boolean; + maxMonths: number; + theme: Theme; + documentDir: string; // Electron only +}>; diff --git a/upcoming-release-notes/1186.md b/upcoming-release-notes/1186.md new file mode 100644 index 000000000..738569ce6 --- /dev/null +++ b/upcoming-release-notes/1186.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [TomAFrench] +--- + +Improve TypeScript types in `loot-core` \ No newline at end of file -- GitLab