From 9c3b85a9f2c436369bf51cf0273ba67ed965859c Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins Date: Wed, 10 Jan 2024 17:46:06 +0000 Subject: [PATCH] :recycle: (typescript) fixing strictNullChecks=true issues --- packages/crdt/src/crdt/merkle.ts | 2 +- .../loot-core/src/client/actions/queries.ts | 3 +- .../loot-core/src/client/reducers/queries.ts | 33 ++++++----- .../src/client/state-types/queries.d.ts | 3 +- packages/loot-core/src/mocks/budget.ts | 4 +- packages/loot-core/src/server/db/index.ts | 11 +++- .../loot-core/src/shared/transactions.test.ts | 59 +++++++++++++------ packages/loot-core/src/shared/transactions.ts | 31 ++++++---- packages/loot-core/src/shared/util.ts | 28 +++++---- .../loot-core/src/types/models/payee.d.ts | 10 ++-- tsconfig.json | 2 + 11 files changed, 121 insertions(+), 65 deletions(-) diff --git a/packages/crdt/src/crdt/merkle.ts b/packages/crdt/src/crdt/merkle.ts index 067fdb52db2..236c93f2cc6 100644 --- a/packages/crdt/src/crdt/merkle.ts +++ b/packages/crdt/src/crdt/merkle.ts @@ -93,7 +93,7 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null { const keys = [...keyset.values()]; keys.sort(); - let diffkey = null; + let diffkey: null | '0' | '1' | '2' = null; // Traverse down the trie through keys that aren't the same. We // traverse down the keys in order. Stop in two cases: either one diff --git a/packages/loot-core/src/client/actions/queries.ts b/packages/loot-core/src/client/actions/queries.ts index 6c64b08c09a..c3b02f2a859 100644 --- a/packages/loot-core/src/client/actions/queries.ts +++ b/packages/loot-core/src/client/actions/queries.ts @@ -1,6 +1,7 @@ import throttle from 'throttleit'; import { send } from '../../platform/client/fetch'; +import { type AccountEntity } from '../../types/models'; import * as constants from '../constants'; import { pushModal } from './modals'; @@ -260,7 +261,7 @@ export function getAccounts() { }; } -export function updateAccount(account) { +export function updateAccount(account: AccountEntity) { return async (dispatch: Dispatch) => { dispatch({ type: constants.UPDATE_ACCOUNT, account }); await send('account-update', account); diff --git a/packages/loot-core/src/client/reducers/queries.ts b/packages/loot-core/src/client/reducers/queries.ts index 3966e5e53da..4f78d0e9c87 100644 --- a/packages/loot-core/src/client/reducers/queries.ts +++ b/packages/loot-core/src/client/reducers/queries.ts @@ -1,6 +1,7 @@ import memoizeOne from 'memoize-one'; import { groupById } from '../../shared/util'; +import { type AccountEntity, type PayeeEntity } from '../../types/models'; import * as constants from '../constants'; import type { Action } from '../state-types'; import type { QueriesState } from '../state-types/queries'; @@ -59,9 +60,7 @@ export function update(state = initialState, action: Action): QueriesState { return { ...state, accounts: state.accounts.map(account => { - // @ts-expect-error Not typed yet if (account.id === action.account.id) { - // @ts-expect-error Not typed yet return { ...account, ...action.account }; } return account; @@ -83,8 +82,12 @@ export function update(state = initialState, action: Action): QueriesState { return state; } -export const getAccountsById = memoizeOne(accounts => groupById(accounts)); -export const getPayeesById = memoizeOne(payees => groupById(payees)); +export const getAccountsById = memoizeOne((accounts: AccountEntity[]) => + groupById(accounts), +); +export const getPayeesById = memoizeOne((payees: PayeeEntity[]) => + groupById(payees), +); export const getCategoriesById = memoizeOne(categoryGroups => { const res = {}; categoryGroups.forEach(group => { @@ -95,14 +98,16 @@ export const getCategoriesById = memoizeOne(categoryGroups => { return res; }); -export const getActivePayees = memoizeOne((payees, accounts) => { - const accountsById = getAccountsById(accounts); +export const getActivePayees = memoizeOne( + (payees: PayeeEntity[], accounts: AccountEntity[]) => { + const accountsById = getAccountsById(accounts); - return payees.filter(payee => { - if (payee.transfer_acct) { - const account = accountsById[payee.transfer_acct]; - return account != null && !account.closed; - } - return true; - }); -}); + return payees.filter(payee => { + if (payee.transfer_acct) { + const account = accountsById[payee.transfer_acct]; + return account != null && !account.closed; + } + return true; + }); + }, +); diff --git a/packages/loot-core/src/client/state-types/queries.d.ts b/packages/loot-core/src/client/state-types/queries.d.ts index 829c9038218..a7f18526fe0 100644 --- a/packages/loot-core/src/client/state-types/queries.d.ts +++ b/packages/loot-core/src/client/state-types/queries.d.ts @@ -1,4 +1,5 @@ import type { Handlers } from '../../types/handlers'; +import { type AccountEntity } from '../../types/models'; import type * as constants from '../constants'; export type QueriesState = { @@ -41,7 +42,7 @@ type LoadAccountsAction = { type UpdateAccountAction = { type: typeof constants.UPDATE_ACCOUNT; - account: unknown; + account: AccountEntity; }; type LoadCategoriesAction = { diff --git a/packages/loot-core/src/mocks/budget.ts b/packages/loot-core/src/mocks/budget.ts index e4d9506030f..b27132ebc46 100644 --- a/packages/loot-core/src/mocks/budget.ts +++ b/packages/loot-core/src/mocks/budget.ts @@ -11,13 +11,13 @@ import * as monthUtils from '../shared/months'; import { q } from '../shared/query'; import type { CategoryGroupEntity, - PayeeEntity, + NewPayeeEntity, NewTransactionEntity, } from '../types/models'; import { random } from './random'; -type MockPayeeEntity = PayeeEntity & { bill?: boolean }; +type MockPayeeEntity = NewPayeeEntity & { bill?: boolean }; function pickRandom(list: T[]): T { return list[Math.floor(random() * list.length) % list.length]; diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts index 1f4fb2bb035..2730363fece 100644 --- a/packages/loot-core/src/server/db/index.ts +++ b/packages/loot-core/src/server/db/index.ts @@ -12,7 +12,11 @@ import { v4 as uuidv4 } from 'uuid'; import * as fs from '../../platform/server/fs'; import * as sqlite from '../../platform/server/sqlite'; import { groupById } from '../../shared/util'; -import { CategoryEntity, CategoryGroupEntity } from '../../types/models'; +import { + CategoryEntity, + CategoryGroupEntity, + PayeeEntity, +} from '../../types/models'; import { schema, schemaConfig, @@ -478,9 +482,10 @@ export function updatePayee(payee) { return update('payees', payee); } -export async function mergePayees(target, ids) { +export async function mergePayees(target: string, ids: string[]) { // Load in payees so we can check some stuff - const payees = groupById(await all('SELECT * FROM payees')); + const dbPayees: PayeeEntity[] = await all('SELECT * FROM payees'); + const payees = groupById(dbPayees); // Filter out any transfer payees if (payees[target].transfer_acct != null) { diff --git a/packages/loot-core/src/shared/transactions.test.ts b/packages/loot-core/src/shared/transactions.test.ts index 2ff17433fa3..1de55fa1e64 100644 --- a/packages/loot-core/src/shared/transactions.test.ts +++ b/packages/loot-core/src/shared/transactions.test.ts @@ -1,5 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; +import { TransactionEntity } from '../types/models'; + import { splitTransaction, updateTransaction, @@ -8,16 +10,26 @@ import { makeChild, } from './transactions'; -// const data = { -// splitTransactions: generateTransaction({ amount: -5000, acct: 2 }, -2000) -// }; - -function makeTransaction(data) { +function makeTransaction(data: Partial): TransactionEntity { return { id: uuidv4(), amount: 2422, date: '2020-01-05', - account: 'acct1', + account: { + id: 'acc-id-1', + name: 'account-1', + offbudget: 0, + closed: 0, + sort_order: 1, + tombstone: 0, + account_id: null, + bank: null, + mask: null, + official_name: null, + balance_current: null, + balance_available: null, + balance_limit: null, + }, ...data, }; } @@ -27,7 +39,7 @@ function makeSplitTransaction(data, children) { return [parent, ...children.map(t => makeChild(parent, t))]; } -function splitError(amount) { +function splitError(amount: number) { return { difference: amount, type: 'SplitTransactionError', version: 1 }; } @@ -38,10 +50,13 @@ describe('Transactions', () => { makeTransaction({ id: 't1', amount: 4000 }), makeTransaction({ amount: 3000 }), ]; - const { data, diff } = updateTransaction(transactions, { - id: 't1', - amount: 5000, - }); + const { data, diff } = updateTransaction( + transactions, + makeTransaction({ + id: 't1', + amount: 5000, + }), + ); expect(data.find(d => d.subtransactions)).toBeFalsy(); expect(diff).toEqual({ added: [], @@ -60,10 +75,13 @@ describe('Transactions', () => { makeTransaction({ id: 't1', amount: 5000 }), makeTransaction({ amount: 3000 }), ]; - const { data, diff } = updateTransaction(transactions, { - id: 't1', - amount: 5000, - }); + const { data, diff } = updateTransaction( + transactions, + makeTransaction({ + id: 't1', + amount: 5000, + }), + ); expect(diff).toEqual({ added: [], deleted: [], updated: [] }); expect(data.map(t => ({ id: t.id, amount: t.amount })).sort()).toEqual([ { id: expect.any(String), amount: 5000 }, @@ -160,10 +178,13 @@ describe('Transactions', () => { ]), makeTransaction({ amount: 3002 }), ]; - const { data, diff } = updateTransaction(transactions, { - id: 't2', - amount: 2200, - }); + const { data, diff } = updateTransaction( + transactions, + makeTransaction({ + id: 't2', + amount: 2200, + }), + ); expect(data.find(d => d.subtransactions)).toBeFalsy(); expect(diff).toEqual({ added: [], diff --git a/packages/loot-core/src/shared/transactions.ts b/packages/loot-core/src/shared/transactions.ts index d12fd0768eb..84ca2166960 100644 --- a/packages/loot-core/src/shared/transactions.ts +++ b/packages/loot-core/src/shared/transactions.ts @@ -111,7 +111,9 @@ export function applyTransactionDiff( groupedTrans: Parameters[0], diff: Parameters[0], ) { - return groupTransaction(applyChanges(diff, ungroupTransaction(groupedTrans))); + return groupTransaction( + applyChanges(diff, ungroupTransaction(groupedTrans) || []), + ); } function replaceTransactions( @@ -131,7 +133,7 @@ function replaceTransactions( const parentIndex = findParentIndex(transactions, idx); if (parentIndex == null) { console.log('Cannot find parent index'); - return { diff: { deleted: [], updated: [] } }; + return { data: [], diff: { deleted: [], updated: [] } }; } const split = getSplit(transactions, parentIndex); @@ -177,8 +179,8 @@ export function addSplitTransaction( if (!trans.is_parent) { return trans; } - const prevSub = last(trans.subtransactions); - trans.subtransactions.push( + const prevSub = last(trans.subtransactions || []); + trans.subtransactions?.push( makeChild(trans, { amount: 0, sort_order: num(prevSub && prevSub.sort_order) - 1, @@ -188,11 +190,14 @@ export function addSplitTransaction( }); } -export function updateTransaction(transactions, transaction) { +export function updateTransaction( + transactions: TransactionEntity[], + transaction: TransactionEntity, +) { return replaceTransactions(transactions, transaction.id, trans => { if (trans.is_parent) { const parent = trans.id === transaction.id ? transaction : trans; - const sub = trans.subtransactions.map(t => { + const sub = trans.subtransactions?.map(t => { // Make sure to update the children to reflect the updated // properties (if the parent updated) @@ -216,12 +221,15 @@ export function updateTransaction(transactions, transaction) { }); } -export function deleteTransaction(transactions, id) { +export function deleteTransaction( + transactions: TransactionEntity[], + id: string, +) { return replaceTransactions(transactions, id, trans => { if (trans.is_parent) { if (trans.id === id) { return null; - } else if (trans.subtransactions.length === 1) { + } else if (trans.subtransactions?.length === 1) { return { ...trans, subtransactions: null, @@ -229,7 +237,7 @@ export function deleteTransaction(transactions, id) { error: null, }; } else { - const sub = trans.subtransactions.filter(t => t.id !== id); + const sub = trans.subtransactions?.filter(t => t.id !== id); return recalculateSplit({ ...trans, subtransactions: sub }); } } else { @@ -238,7 +246,10 @@ export function deleteTransaction(transactions, id) { }); } -export function splitTransaction(transactions, id) { +export function splitTransaction( + transactions: TransactionEntity[], + id: string, +) { return replaceTransactions(transactions, id, trans => { if (trans.is_parent || trans.is_child) { return trans; diff --git a/packages/loot-core/src/shared/util.ts b/packages/loot-core/src/shared/util.ts index 3c32ab5cd70..d3bf5d266e3 100644 --- a/packages/loot-core/src/shared/util.ts +++ b/packages/loot-core/src/shared/util.ts @@ -2,13 +2,17 @@ export function last(arr: Array) { return arr[arr.length - 1]; } -export function getChangedValues(obj1, obj2) { - // Keep the id field because this is mostly used to diff database - // objects - const diff = obj1.id ? { id: obj1.id } : {}; +export function getChangedValues(obj1: T, obj2: T) { + const diff: Partial = {}; const keys = Object.keys(obj2); let hasChanged = false; + // Keep the id field because this is mostly used to diff database + // objects + if (obj1.id) { + diff.id = obj1.id; + } + for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -21,7 +25,11 @@ export function getChangedValues(obj1, obj2) { return hasChanged ? diff : null; } -export function hasFieldsChanged(obj1, obj2, fields) { +export function hasFieldsChanged( + obj1: T, + obj2: T, + fields: Array, +) { let changed = false; for (let i = 0; i < fields.length; i++) { const field = fields[i]; @@ -101,7 +109,7 @@ export function groupBy(data: T[], field: K) { // different API and we need to go through and update everywhere that // uses it. function _groupById(data: T[]) { - const res = new Map(); + const res = new Map(); for (let i = 0; i < data.length; i++) { const item = data[i]; res.set(item.id, item); @@ -112,8 +120,8 @@ function _groupById(data: T[]) { export function diffItems(items: T[], newItems: T[]) { const grouped = _groupById(items); const newGrouped = _groupById(newItems); - const added = []; - const updated = []; + const added: T[] = []; + const updated: Partial[] = []; const deleted = items .filter(item => !newGrouped.has(item.id)) @@ -135,7 +143,7 @@ export function diffItems(items: T[], newItems: T[]) { } export function groupById(data: T[]) { - const res = {}; + const res: { [key: string]: T } = {}; for (let i = 0; i < data.length; i++) { const item = data[i]; res[item.id] = item; @@ -358,7 +366,7 @@ export function looselyParseAmount(amount: string) { } const m = amount.match(/[.,][^.,]*$/); - if (!m || m.index === 0) { + if (!m || m.index === undefined || m.index === 0) { return safeNumber(parseFloat(extractNumbers(amount))); } diff --git a/packages/loot-core/src/types/models/payee.d.ts b/packages/loot-core/src/types/models/payee.d.ts index 381f840b126..5d84bc16bcf 100644 --- a/packages/loot-core/src/types/models/payee.d.ts +++ b/packages/loot-core/src/types/models/payee.d.ts @@ -1,8 +1,10 @@ -import type { AccountEntity } from './account'; - -export interface PayeeEntity { +export interface NewPayeeEntity { id?: string; name: string; - transfer_acct?: AccountEntity; + transfer_acct?: string; tombstone?: boolean; } + +export interface PayeeEntity extends NewPayeeEntity { + id: string; +} diff --git a/tsconfig.json b/tsconfig.json index a426766d86a..bce7b344d2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,8 @@ "downlevelIteration": true, // TODO: enable once every file is ts // "strict": true, + // TODO: enable once all issues fixed + // "strictNullChecks": true, "strictFunctionTypes": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true,