From 9ae74c0d1bd36e22f357895cdf48e7906b83edac Mon Sep 17 00:00:00 2001 From: Tom French Date: Fri, 2 Jun 2023 06:07:37 +0100 Subject: [PATCH 01/21] chore: add concrete types in more areas --- .../loot-core/src/server/budget/actions.ts | 125 +++++++++++++++--- packages/loot-core/src/server/main.ts | 2 +- packages/loot-core/src/server/prefs.ts | 55 +++++--- .../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/shared/months.ts | 72 ++++++---- packages/loot-core/src/shared/util.ts | 2 +- 10 files changed, 278 insertions(+), 107 deletions(-) diff --git a/packages/loot-core/src/server/budget/actions.ts b/packages/loot-core/src/server/budget/actions.ts index 2b0c527e487..6983ac7650a 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 { 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'; } -function isReflectBudget() { +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 { 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 { 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 { 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 { 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 { 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,7 @@ export async function copyPreviousMonth({ month }) { }); } -export async function setZero({ month }) { +export async function setZero({ month }: { month: string }): Promise { let categories = await db.all( 'SELECT * FROM v_categories WHERE tombstone = 0', ); @@ -151,7 +188,11 @@ export async function setZero({ month }) { }); } -export async function set3MonthAvg({ month }) { +export async function set3MonthAvg({ + month, +}: { + month: string; +}): Promise { let categories = await db.all( 'SELECT * FROM v_categories WHERE tombstone = 0', ); @@ -185,7 +226,13 @@ export async function set3MonthAvg({ month }) { }); } -export async function holdForNextMonth({ month, amount }) { +export async function holdForNextMonth({ + month, + amount, +}: { + month: string; + amount: number; +}): Promise { let row = await db.first( 'SELECT buffered FROM zero_budget_months WHERE id = ?', [month], @@ -207,11 +254,19 @@ export async function holdForNextMonth({ month, amount }) { return false; } -export async function resetHold({ month }) { +export async function resetHold({ month }: { month: string }): Promise { 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 { let sheetName = monthUtils.sheetForMonth(month); let toBudgeted = await getSheetValue(sheetName, 'budget-' + to); let leftover = await getSheetValue(sheetName, 'leftover-' + to); @@ -239,7 +294,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 { let sheetName = monthUtils.sheetForMonth(month); let leftover = await getSheetValue(sheetName, 'to-budget'); amount = Math.max(Math.min(amount, leftover), 0); @@ -248,7 +311,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 { const sheetName = monthUtils.sheetForMonth(month); const fromBudgeted = await getSheetValue(sheetName, 'budget-' + from); @@ -262,13 +335,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 { 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.ts b/packages/loot-core/src/server/main.ts index cb8069813db..f584680881a 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -260,7 +260,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 46ced65dda1..2d3f6d5703b 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -2,14 +2,36 @@ import { Timestamp } from '@actual-app/crdt'; import * as fs from '../platform/server/fs'; -import { sendMessages } from './sync'; - -let prefs = null; - -export async function loadPrefs(id?) { +import { Message, sendMessages } from './sync'; + +export const BUDGET_TYPES = ['report', 'rollover'] as const; +export type BudgetType = (typeof BUDGET_TYPES)[number]; + +type Preferences = { + id: string; + budgetName: string; + budgetType?: BudgetType; + clientId?: string; + groupId?: string; + userId?: string; + lastSyncedTimestamp?: string; + resetClock?: boolean; + cloudFileId?: string; + lastUploaded?: string; + encryptKeyId?: string; + 'notifications.schedules'?: boolean; + 'notifications.repair-splits'?: boolean; + dummyTestPrefs?: boolean; +}; + +let prefs: Preferences = null; + +export async function loadPrefs( + id?: string, +): Promise { if (process.env.NODE_ENV === 'test' && !id) { - prefs = { dummyTestPrefs: true }; - return prefs; + // TODO: check if we can remove this as it seems to be unused. + return { dummyTestPrefs: true }; } const fullpath = fs.join(fs.getBudgetDir(id), 'metadata.json'); @@ -42,12 +64,15 @@ export async function loadPrefs(id?) { return prefs; } -export async function savePrefs(prefsToSet, { avoidSync = false } = {}) { +export async function savePrefs( + prefsToSet: Partial, + { avoidSync = false } = {}, +): Promise { 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 { @@ -60,32 +85,32 @@ export async function savePrefs(prefsToSet, { avoidSync = false } = {}) { } return null; }) - .filter(x => x); + .filter(x => x !== null); if (messages.length > 0) { await sendMessages(messages); } } - 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(): Preferences { return prefs; } -export function getDefaultPrefs(id, budgetName) { +export function getDefaultPrefs(id: string, budgetName: string): Preferences { return { id, budgetName }; } -export async function readPrefs(id) { +export async function readPrefs(id: string): Promise { const fullpath = fs.join(fs.getBudgetDir(id), 'metadata.json'); try { diff --git a/packages/loot-core/src/server/server-config.ts b/packages/loot-core/src/server/server-config.ts index 56f62cd10e1..9b8dacac845 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; + NORDIGEN_SERVER: string; +}; -function joinURL(base, ...paths) { +let config: ServerConfig = 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 { 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 8eb5341c011..4c67f935e75 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 { 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 { if (globalSheet) { unloadSpreadsheet(); return loadSpreadsheet(db, globalOnChange); } } -export async function loadUserBudgets(db) { +export async function loadUserBudgets(db): Promise { 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 { 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 45b65640961..8bc5256749b 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 cab86110654..53f62431d52 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; 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(); 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 92709cffa52..001772ed343 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/shared/months.ts b/packages/loot-core/src/shared/months.ts index 7a48d7be4f7..eec7f8b04f7 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -1,7 +1,7 @@ import * as d from 'date-fns'; import memoizeOne from 'memoize-one'; -function _parse(value: string | Date) { +function _parse(value: string | Date): 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 @@ -71,19 +71,19 @@ function _parse(value: string | Date) { export const parseDate = _parse; -export function yearFromDate(date) { +export function yearFromDate(date: string | Date): string { return d.format(_parse(date), 'yyyy'); } -export function monthFromDate(date) { +export function monthFromDate(date: string | Date): string { return d.format(_parse(date), 'yyyy-MM'); } -export function dayFromDate(date) { +export function dayFromDate(date: string | Date): string { return d.format(_parse(date), 'yyyy-MM-dd'); } -export function currentMonth() { +export function currentMonth(): string { if (global.IS_TESTING) { return global.currentMonth || '2017-01'; } else { @@ -91,7 +91,7 @@ export function currentMonth() { } } -export function currentDay() { +export function currentDay(): string { if (global.IS_TESTING) { return '2017-01-01'; } else { @@ -99,48 +99,55 @@ export function currentDay() { } } -export function nextMonth(month) { +export function nextMonth(month: string | Date): string { return d.format(d.addMonths(_parse(month), 1), 'yyyy-MM'); } -export function prevMonth(month) { +export function prevMonth(month: string | Date): string { return d.format(d.subMonths(_parse(month), 1), 'yyyy-MM'); } -export function addMonths(month, n) { +export function addMonths(month: string | Date, n: number): string { return d.format(d.addMonths(_parse(month), n), 'yyyy-MM'); } -export function subMonths(month, n) { +export function subMonths(month: string | Date, n: number): string { return d.format(d.subMonths(_parse(month), n), 'yyyy-MM'); } -export function addDays(day, n) { +export function addDays(day: string | Date, n: number): string { return d.format(d.addDays(_parse(day), n), 'yyyy-MM-dd'); } -export function subDays(day, n) { +export function subDays(day: string | Date, n: number): string { return d.format(d.subDays(_parse(day), n), 'yyyy-MM-dd'); } -export function isBefore(month1, month2) { +export function isBefore( + month1: string | Date, + month2: string | Date, +): boolean { return d.isBefore(_parse(month1), _parse(month2)); } -export function isAfter(month1, month2) { +export function isAfter(month1: string | Date, month2: string | Date): 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) { +export function bounds(month: string | Date): { start: number; end: number } { return { start: parseInt(d.format(d.startOfMonth(_parse(month)), 'yyyyMMdd')), end: parseInt(d.format(d.endOfMonth(_parse(month)), 'yyyyMMdd')), }; } -export function _range(start, end, inclusive = false) { +export function _range( + start: string | Date, + end: string | Date, + inclusive = false, +): string[] { const months = []; let month = monthFromDate(start); while (d.isBefore(_parse(month), _parse(end))) { @@ -155,15 +162,22 @@ export function _range(start, end, inclusive = false) { return months; } -export function range(start, end) { +export function range(start: string | Date, end: string | Date): string[] { return _range(start, end); } -export function rangeInclusive(start, end) { +export function rangeInclusive( + start: string | Date, + end: string | Date, +): string[] { return _range(start, end, true); } -export function _dayRange(start, end, inclusive = false) { +export function _dayRange( + start: string | Date, + end: string | Date, + inclusive = false, +): string[] { const days = []; let day = start; while (d.isBefore(_parse(day), _parse(end))) { @@ -178,44 +192,44 @@ export function _dayRange(start, end, inclusive = false) { return days; } -export function dayRange(start, end) { +export function dayRange(start: string | Date, end: string | Date) { return _dayRange(start, end); } -export function dayRangeInclusive(start, end) { +export function dayRangeInclusive(start: string | Date, end: string | Date) { return _dayRange(start, end, true); } -export function getMonthIndex(month) { +export function getMonthIndex(month: string): number { return parseInt(month.slice(5, 7)) - 1; } -export function getYear(month) { +export function getYear(month: string): string { return month.slice(0, 4); } -export function getMonth(day) { +export function getMonth(day: string): string { return day.slice(0, 7); } -export function getYearStart(month) { +export function getYearStart(month: string): string { return getYear(month) + '-01'; } -export function getYearEnd(month) { +export function getYearEnd(month: string): string { return getYear(month) + '-12'; } -export function sheetForMonth(month) { +export function sheetForMonth(month: string): string { return 'budget' + month.replace('-', ''); } -export function nameForMonth(month) { +export function nameForMonth(month: string): string { // eslint-disable-next-line rulesdir/typography return d.format(_parse(month), "MMMM 'yy"); } -export function format(month, str) { +export function format(month: string, 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 f1dec4b3332..aa872605cd3 100644 --- a/packages/loot-core/src/shared/util.ts +++ b/packages/loot-core/src/shared/util.ts @@ -256,7 +256,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), From a96705efd28c01ef61bfe1a33f26521eca683032 Mon Sep 17 00:00:00 2001 From: Tom French Date: Sun, 25 Jun 2023 15:43:44 +0100 Subject: [PATCH 02/21] chore: add a shared `DateLike` type alias for `months.ts` --- packages/loot-core/src/shared/months.ts | 50 ++++++++++++------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index eec7f8b04f7..d88c36c3afa 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'; -function _parse(value: string | Date): Date { +type DateLike = string | Date; + +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 @@ -71,15 +73,15 @@ function _parse(value: string | Date): Date { export const parseDate = _parse; -export function yearFromDate(date: string | Date): string { +export function yearFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy'); } -export function monthFromDate(date: string | Date): string { +export function monthFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy-MM'); } -export function dayFromDate(date: string | Date): string { +export function dayFromDate(date: DateLike): string { return d.format(_parse(date), 'yyyy-MM-dd'); } @@ -99,44 +101,41 @@ export function currentDay(): string { } } -export function nextMonth(month: string | Date): string { +export function nextMonth(month: DateLike): string { return d.format(d.addMonths(_parse(month), 1), 'yyyy-MM'); } -export function prevMonth(month: string | Date): string { +export function prevMonth(month: DateLike): string { return d.format(d.subMonths(_parse(month), 1), 'yyyy-MM'); } -export function addMonths(month: string | Date, n: number): string { +export function addMonths(month: DateLike, n: number): string { return d.format(d.addMonths(_parse(month), n), 'yyyy-MM'); } -export function subMonths(month: string | Date, n: number): string { +export function subMonths(month: DateLike, n: number): string { return d.format(d.subMonths(_parse(month), n), 'yyyy-MM'); } -export function addDays(day: string | Date, n: number): string { +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): string { +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, -): boolean { +export function isBefore(month1: DateLike, month2: DateLike): boolean { return d.isBefore(_parse(month1), _parse(month2)); } -export function isAfter(month1: string | Date, month2: string | Date): boolean { +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): { start: number; end: number } { +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')), @@ -144,8 +143,8 @@ export function bounds(month: string | Date): { start: number; end: number } { } export function _range( - start: string | Date, - end: string | Date, + start: DateLike, + end: DateLike, inclusive = false, ): string[] { const months = []; @@ -162,20 +161,17 @@ export function _range( return months; } -export function range(start: string | Date, end: string | Date): string[] { +export function range(start: DateLike, end: DateLike): string[] { return _range(start, end); } -export function rangeInclusive( - start: string | Date, - end: string | Date, -): string[] { +export function rangeInclusive(start: DateLike, end: DateLike): string[] { return _range(start, end, true); } export function _dayRange( - start: string | Date, - end: string | Date, + start: DateLike, + end: DateLike, inclusive = false, ): string[] { const days = []; @@ -192,11 +188,11 @@ export function _dayRange( return days; } -export function dayRange(start: string | Date, end: string | Date) { +export function dayRange(start: DateLike, end: DateLike) { return _dayRange(start, end); } -export function dayRangeInclusive(start: string | Date, end: string | Date) { +export function dayRangeInclusive(start: DateLike, end: DateLike) { return _dayRange(start, end, true); } From c1a74ec22dbc6442780c1f62e76ef3c4605eb9c0 Mon Sep 17 00:00:00 2001 From: Tom French Date: Sun, 25 Jun 2023 15:57:35 +0100 Subject: [PATCH 03/21] chore: fix type issues --- packages/loot-core/src/mocks/budget.ts | 21 ++++++++++++--------- packages/loot-core/src/server/prefs.ts | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/loot-core/src/mocks/budget.ts b/packages/loot-core/src/mocks/budget.ts index ba559dc747d..84536457142 100644 --- a/packages/loot-core/src/mocks/budget.ts +++ b/packages/loot-core/src/mocks/budget.ts @@ -141,7 +141,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) { ); let currentDay = monthUtils.currentDay(); for (let month of months) { - let date = monthUtils.addDays(month, '12'); + let date = monthUtils.addDays(month, 12); if (monthUtils.isBefore(date, currentDay)) { transactions.push({ amount: -10000, @@ -152,7 +152,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) { }); } - date = monthUtils.addDays(month, '18'); + date = monthUtils.addDays(month, 18); if (monthUtils.isBefore(date, currentDay)) { transactions.push({ amount: -9000, @@ -163,7 +163,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) { }); } - date = monthUtils.addDays(month, '2'); + date = monthUtils.addDays(month, 2); if (monthUtils.isBefore(date, currentDay)) { transactions.push({ amount: -120000, @@ -174,7 +174,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) { }); } - date = monthUtils.addDays(month, '20'); + date = monthUtils.addDays(month, 20); if (monthUtils.isBefore(date, currentDay)) { transactions.push({ amount: -6000, @@ -186,7 +186,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) { }); } - date = monthUtils.addDays(month, '23'); + date = monthUtils.addDays(month, 23); if (monthUtils.isBefore(date, currentDay)) { transactions.push({ amount: -7500, @@ -454,10 +454,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); @@ -515,7 +515,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) { @@ -534,7 +537,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/prefs.ts b/packages/loot-core/src/server/prefs.ts index 2d3f6d5703b..fa6c1c260e5 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -22,6 +22,7 @@ type Preferences = { 'notifications.schedules'?: boolean; 'notifications.repair-splits'?: boolean; dummyTestPrefs?: boolean; + isCached?: boolean; }; let prefs: Preferences = null; From beae5764559add7b36d42dd08c1fca558793a2f8 Mon Sep 17 00:00:00 2001 From: Tom French Date: Sun, 25 Jun 2023 16:08:33 +0100 Subject: [PATCH 04/21] chore: fix type errors --- .../src/components/budget/rollover/BudgetSummary.tsx | 2 +- packages/loot-core/src/server/accounts/parse-file.ts | 2 +- packages/loot-core/src/server/prefs.ts | 1 + packages/loot-core/src/shared/months.ts | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx index b4b1e6b5d0c..15afe636f19 100644 --- a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx +++ b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx @@ -256,7 +256,7 @@ function ToBudget({ } type BudgetSummaryProps = { - month: string | number; + month: string; isGoalTemplatesEnabled: boolean; }; export function BudgetSummary({ diff --git a/packages/loot-core/src/server/accounts/parse-file.ts b/packages/loot-core/src/server/accounts/parse-file.ts index 8819629fb04..3aaaf855d8e 100644 --- a/packages/loot-core/src/server/accounts/parse-file.ts +++ b/packages/loot-core/src/server/accounts/parse-file.ts @@ -111,7 +111,7 @@ async function parseOFX(filepath) { 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: trans.name, imported_payee: trans.name, notes: trans.memo || null, diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index fa6c1c260e5..75d76cb7cdc 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -15,6 +15,7 @@ type Preferences = { groupId?: string; userId?: string; lastSyncedTimestamp?: string; + lastScheduleRun?: string; resetClock?: boolean; cloudFileId?: string; lastUploaded?: string; diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index d88c36c3afa..ce6c5cf3d5d 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -220,12 +220,12 @@ export function sheetForMonth(month: string): string { return 'budget' + month.replace('-', ''); } -export function nameForMonth(month: string): string { +export function nameForMonth(month: DateLike): string { // eslint-disable-next-line rulesdir/typography return d.format(_parse(month), "MMMM 'yy"); } -export function format(month: string, str: string): string { +export function format(month: DateLike, str: string): string { return d.format(_parse(month), str); } From ca73a60409b3f88d0828e416cafc1ae6acd69797 Mon Sep 17 00:00:00 2001 From: Tom French Date: Sun, 25 Jun 2023 16:11:20 +0100 Subject: [PATCH 05/21] chore: add patch note --- upcoming-release-notes/1186.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 upcoming-release-notes/1186.md diff --git a/upcoming-release-notes/1186.md b/upcoming-release-notes/1186.md new file mode 100644 index 00000000000..738569ce6c0 --- /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 From e8ff4a553da7ad412e141395e7e50a258fdb5383 Mon Sep 17 00:00:00 2001 From: Tom French Date: Sun, 25 Jun 2023 16:27:13 +0100 Subject: [PATCH 06/21] chore: fix test preferences --- packages/loot-core/src/server/prefs.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index 75d76cb7cdc..081989e22f5 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -22,7 +22,6 @@ type Preferences = { encryptKeyId?: string; 'notifications.schedules'?: boolean; 'notifications.repair-splits'?: boolean; - dummyTestPrefs?: boolean; isCached?: boolean; }; @@ -30,10 +29,11 @@ let prefs: Preferences = null; export async function loadPrefs( id?: string, -): Promise { +): Promise { if (process.env.NODE_ENV === 'test' && !id) { - // TODO: check if we can remove this as it seems to be unused. - return { dummyTestPrefs: true }; + // Needed so that we can make preferences object non-null for testing. + prefs = getDefaultPrefs("test", "test_preferences") + return prefs; } const fullpath = fs.join(fs.getBudgetDir(id), 'metadata.json'); From 78f228ba55cdddc0da90bc47a48e06105a26e0bd Mon Sep 17 00:00:00 2001 From: Tom French Date: Sun, 25 Jun 2023 16:41:53 +0100 Subject: [PATCH 07/21] chore: linter --- packages/loot-core/src/server/prefs.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index 081989e22f5..cc6c43bdedc 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -27,12 +27,10 @@ type Preferences = { let prefs: Preferences = null; -export async function loadPrefs( - id?: string, -): Promise { +export async function loadPrefs(id?: string): Promise { if (process.env.NODE_ENV === 'test' && !id) { // Needed so that we can make preferences object non-null for testing. - prefs = getDefaultPrefs("test", "test_preferences") + prefs = getDefaultPrefs("test", "test_preferences"); return prefs; } From 3576fd6513f86c371e8aee176801dbd6895eab29 Mon Sep 17 00:00:00 2001 From: Tom French Date: Mon, 26 Jun 2023 19:13:10 +0100 Subject: [PATCH 08/21] chore: linter --- packages/loot-core/src/server/prefs.ts | 2 +- packages/loot-core/src/shared/months.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index cc6c43bdedc..35756526fff 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -30,7 +30,7 @@ let prefs: Preferences = null; export async function loadPrefs(id?: string): Promise { if (process.env.NODE_ENV === 'test' && !id) { // Needed so that we can make preferences object non-null for testing. - prefs = getDefaultPrefs("test", "test_preferences"); + prefs = getDefaultPrefs('test', 'test_preferences'); return prefs; } diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index c33a902033a..42233605eec 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -1,6 +1,5 @@ import * as d from 'date-fns'; import memoizeOne from 'memoize-one'; -import { DelegatedPlugin } from 'webpack'; type DateLike = string | Date; From 159ea24bb85385a97247dde1500932cd8756801f Mon Sep 17 00:00:00 2001 From: Tom French Date: Mon, 26 Jun 2023 19:16:28 +0100 Subject: [PATCH 09/21] chore: linter --- packages/loot-core/src/shared/months.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index 42233605eec..f1a805d023e 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -117,7 +117,10 @@ export function addWeeks(date: DateLike, n: number): string { return d.format(d.addWeeks(_parse(date), n), 'yyyy-MM-dd'); } -export function differenceInCalendarMonths(month1: DateLike, month2: DateLike): number { +export function differenceInCalendarMonths( + month1: DateLike, + month2: DateLike +): number { return d.differenceInCalendarMonths(_parse(month1), _parse(month2)); } From 89f291c90710f076aa14e91d1e963ad20d9b5286 Mon Sep 17 00:00:00 2001 From: Tom French Date: Wed, 28 Jun 2023 19:08:51 +0100 Subject: [PATCH 10/21] chore: add typecasts --- packages/loot-core/src/server/api.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/loot-core/src/server/api.ts b/packages/loot-core/src/server/api.ts index 2d67d5e9c06..194a9e61f8d 100644 --- a/packages/loot-core/src/server/api.ts +++ b/packages/loot-core/src/server/api.ts @@ -298,16 +298,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) { From b09602721250daf9a6e7f9bf8083e45196aae77a Mon Sep 17 00:00:00 2001 From: Tom French Date: Tue, 4 Jul 2023 13:41:38 +0100 Subject: [PATCH 11/21] chore: linter --- packages/loot-core/src/shared/months.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index aea8d548d9e..9d436380ae2 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -121,8 +121,8 @@ export function addWeeks(date: DateLike, n: number): string { } export function differenceInCalendarMonths( - month1: DateLike, - month2: DateLike + month1: DateLike, + month2: DateLike, ): number { return d.differenceInCalendarMonths(_parse(month1), _parse(month2)); } From b80b2bdc80b1879646f0ba501e19569eb81eb017 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:42:48 -0700 Subject: [PATCH 12/21] Merge independent prefs types --- .../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 | 56 +------------------ packages/loot-core/src/server/prefs.ts | 37 ++++-------- packages/loot-core/src/types/prefs.d.ts | 53 ++++++++++++++++++ 6 files changed, 67 insertions(+), 85 deletions(-) create mode 100644 packages/loot-core/src/types/prefs.d.ts diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx index e2478e95d62..d0fe855623a 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 6d47a634e48..fcb97731be3 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 = { reportBudget: false, diff --git a/packages/desktop-client/src/style/theme.tsx b/packages/desktop-client/src/style/theme.tsx index 6f349bee110..b87fc90fd1b 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 7611242dc0d..21f167fa025 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 = { [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; diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index 35756526fff..f9de43ccdf9 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -1,36 +1,19 @@ import { Timestamp } from '@actual-app/crdt'; import * as fs from '../platform/server/fs'; +import type { LocalPrefs } from '../types/prefs'; import { Message, sendMessages } from './sync'; export const BUDGET_TYPES = ['report', 'rollover'] as const; export type BudgetType = (typeof BUDGET_TYPES)[number]; -type Preferences = { - id: string; - budgetName: string; - budgetType?: BudgetType; - clientId?: string; - groupId?: string; - userId?: string; - lastSyncedTimestamp?: string; - lastScheduleRun?: string; - resetClock?: boolean; - cloudFileId?: string; - lastUploaded?: string; - encryptKeyId?: string; - 'notifications.schedules'?: boolean; - 'notifications.repair-splits'?: boolean; - isCached?: boolean; -}; - -let prefs: Preferences = null; - -export async function loadPrefs(id?: string): Promise { +let prefs: LocalPrefs = null; + +export async function loadPrefs(id?: string): Promise { if (process.env.NODE_ENV === 'test' && !id) { - // Needed so that we can make preferences object non-null for testing. - prefs = getDefaultPrefs('test', 'test_preferences'); + // Needed so that we can make LocalPrefs object non-null for testing. + prefs = getDefaultPrefs('test', 'test_LocalPrefs'); return prefs; } @@ -65,7 +48,7 @@ export async function loadPrefs(id?: string): Promise { } export async function savePrefs( - prefsToSet: Partial, + prefsToSet: Partial, { avoidSync = false } = {}, ): Promise { Object.assign(prefs, prefsToSet); @@ -102,15 +85,15 @@ export function unloadPrefs(): void { prefs = null; } -export function getPrefs(): Preferences { +export function getPrefs(): LocalPrefs { return prefs; } -export function getDefaultPrefs(id: string, budgetName: string): Preferences { +export function getDefaultPrefs(id: string, budgetName: string): LocalPrefs { return { id, budgetName }; } -export async function readPrefs(id: string): Promise { +export async function readPrefs(id: string): Promise { const fullpath = fs.join(fs.getBudgetDir(id), 'metadata.json'); try { 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 00000000000..4dcd19128b5 --- /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 +}>; From de07c5fe20a154f4cd26f17c9ad5771265704781 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:44:23 -0700 Subject: [PATCH 13/21] s/nordigen/gocardless/ --- packages/loot-core/src/server/server-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loot-core/src/server/server-config.ts b/packages/loot-core/src/server/server-config.ts index b0247acacd8..883ff7af439 100644 --- a/packages/loot-core/src/server/server-config.ts +++ b/packages/loot-core/src/server/server-config.ts @@ -5,7 +5,7 @@ type ServerConfig = { SYNC_SERVER: string; SIGNUP_SERVER: string; PLAID_SERVER: string; - NORDIGEN_SERVER: string; + GOCARDLESS_SERVER: string; }; let config: ServerConfig = null; From e3cd905610e0db47219b4022d0c5f93056bf4e5f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:46:13 -0700 Subject: [PATCH 14/21] Remove unused clientId pref --- packages/loot-core/src/server/main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loot-core/src/server/main.test.ts b/packages/loot-core/src/server/main.test.ts index bcb709f0953..8854ae5747c 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 From f9196f3d6de166109c4789a99848ad9a4eefde3d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:47:09 -0700 Subject: [PATCH 15/21] Remove Partial> wrapping --- packages/loot-core/src/client/state-types/prefs.d.ts | 4 ++-- packages/loot-core/src/server/prefs.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 21f167fa025..21ad02f3dac 100644 --- a/packages/loot-core/src/client/state-types/prefs.d.ts +++ b/packages/loot-core/src/client/state-types/prefs.d.ts @@ -14,12 +14,12 @@ export type SetPrefsAction = { export type MergeLocalPrefsAction = { type: typeof constants.MERGE_LOCAL_PREFS; - prefs: Partial; + prefs: LocalPrefs; }; export type MergeGlobalPrefsAction = { type: typeof constants.MERGE_GLOBAL_PREFS; - globalPrefs: Partial; + globalPrefs: GlobalPrefs; }; export type PrefsActions = diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index f9de43ccdf9..398c2f8ad37 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -48,7 +48,7 @@ export async function loadPrefs(id?: string): Promise { } export async function savePrefs( - prefsToSet: Partial, + prefsToSet: LocalPrefs, { avoidSync = false } = {}, ): Promise { Object.assign(prefs, prefsToSet); From b6fffecac3427a24ce67d65ade50f54e47bd0bf1 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:47:57 -0700 Subject: [PATCH 16/21] Fix type check warnings --- packages/loot-core/src/server/sync/encoder.ts | 2 +- packages/loot-core/src/server/sync/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/loot-core/src/server/sync/encoder.ts b/packages/loot-core/src/server/sync/encoder.ts index fc6df6c3291..36aca1d5ecd 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 { 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 c3ec89329fe..34dceb7dd1f 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 = {}; + let prefsToSet: LocalPrefs = {}; let oldData = await fetchData(); undo.appendMessages(messages, oldData); From fa0d67a1b458bd16ce07234e7ca882dcbe451a04 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:49:18 -0700 Subject: [PATCH 17/21] Remove unused readPrefs --- packages/loot-core/src/server/prefs.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index 398c2f8ad37..2bdc24d56c6 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -92,13 +92,3 @@ export function getPrefs(): LocalPrefs { export function getDefaultPrefs(id: string, budgetName: string): LocalPrefs { return { id, budgetName }; } - -export async function readPrefs(id: string): Promise { - const fullpath = fs.join(fs.getBudgetDir(id), 'metadata.json'); - - try { - return JSON.parse(await fs.readFile(fullpath)); - } catch (e) { - return null; - } -} From 262ec2b6f53c77d09f6b99936861856d2e473b5f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:49:29 -0700 Subject: [PATCH 18/21] Remove unused `number` member of DateLike union --- packages/loot-core/src/shared/months.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index 9d436380ae2..df8248fe657 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -1,7 +1,7 @@ import * as d from 'date-fns'; import memoizeOne from 'memoize-one'; -type DateLike = string | number | Date; +type DateLike = string | Date; export function _parse(value: DateLike): Date { if (typeof value === 'string') { From 21e4bdd56702a325114f482fe1a2eaa5921a6c4e Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:50:21 -0700 Subject: [PATCH 19/21] Remove unneeded type casts --- packages/loot-core/src/mocks/budget.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/loot-core/src/mocks/budget.ts b/packages/loot-core/src/mocks/budget.ts index 2d592cf7bd4..6c52454a7f7 100644 --- a/packages/loot-core/src/mocks/budget.ts +++ b/packages/loot-core/src/mocks/budget.ts @@ -456,7 +456,7 @@ async function createBudget(accounts, payees, groups) { let spent: number = sheet.getCellValue( monthUtils.sheetForMonth(month), `sum-amount-${cat.id}`, - ) as number; + ); if (spent < 0) { setBudget(month, cat, -spent); @@ -514,10 +514,7 @@ async function createBudget(accounts, payees, groups) { month <= monthUtils.currentMonth() ) { let sheetName = monthUtils.sheetForMonth(month); - let toBudget: number = sheet.getCellValue( - sheetName, - 'to-budget', - ) as number; + let toBudget: number = sheet.getCellValue(sheetName, 'to-budget'); let available = toBudget - prevSaved; if (available - 403000 > 0) { @@ -536,7 +533,7 @@ async function createBudget(accounts, payees, groups) { await sheet.waitOnSpreadsheet(); let sheetName = monthUtils.sheetForMonth(monthUtils.currentMonth()); - let toBudget: number = sheet.getCellValue(sheetName, 'to-budget') as number; + let toBudget: number = sheet.getCellValue(sheetName, 'to-budget'); if (toBudget < 0) { await addTransactions(primaryAccount.id, [ { From 259c102e740fb5bb8fa8791178882dd4bbff2212 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:52:46 -0700 Subject: [PATCH 20/21] Revert "Remove unneeded type casts" This reverts commit 21e4bdd56702a325114f482fe1a2eaa5921a6c4e. --- packages/loot-core/src/mocks/budget.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/loot-core/src/mocks/budget.ts b/packages/loot-core/src/mocks/budget.ts index 6c52454a7f7..2d592cf7bd4 100644 --- a/packages/loot-core/src/mocks/budget.ts +++ b/packages/loot-core/src/mocks/budget.ts @@ -456,7 +456,7 @@ async function createBudget(accounts, payees, groups) { 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: number = 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: number = sheet.getCellValue(sheetName, 'to-budget'); + let toBudget: number = sheet.getCellValue(sheetName, 'to-budget') as number; if (toBudget < 0) { await addTransactions(primaryAccount.id, [ { From 2dbb4a1529d459118583b988538ddb8c18f3af03 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 29 Jul 2023 07:57:19 -0700 Subject: [PATCH 21/21] Improve null typing, remove unnecessary change --- packages/loot-core/src/server/prefs.ts | 3 +-- packages/loot-core/src/server/server-config.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts index 2bdc24d56c6..0cb45bcdcd9 100644 --- a/packages/loot-core/src/server/prefs.ts +++ b/packages/loot-core/src/server/prefs.ts @@ -12,7 +12,6 @@ let prefs: LocalPrefs = null; export async function loadPrefs(id?: string): Promise { if (process.env.NODE_ENV === 'test' && !id) { - // Needed so that we can make LocalPrefs object non-null for testing. prefs = getDefaultPrefs('test', 'test_LocalPrefs'); return prefs; } @@ -68,7 +67,7 @@ export async function savePrefs( } return null; }) - .filter(x => x !== null); + .filter(x => x); if (messages.length > 0) { await sendMessages(messages); diff --git a/packages/loot-core/src/server/server-config.ts b/packages/loot-core/src/server/server-config.ts index 883ff7af439..623cced37c4 100644 --- a/packages/loot-core/src/server/server-config.ts +++ b/packages/loot-core/src/server/server-config.ts @@ -8,7 +8,7 @@ type ServerConfig = { GOCARDLESS_SERVER: string; }; -let config: ServerConfig = null; +let config: ServerConfig | null = null; function joinURL(base: string | URL, ...paths: string[]): string { let url = new URL(base); @@ -25,7 +25,7 @@ export function setServer(url: string): void { } // `url` is optional; if not given it will provide the global config -export function getServer(url?: string): ServerConfig { +export function getServer(url?: string): ServerConfig | null { if (url) { return { BASE_SERVER: url,