Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] [TypeScript] Make db.select functions generic to make it easy to type DB query results #4251

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
8 changes: 4 additions & 4 deletions packages/loot-core/src/mocks/budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,14 +461,14 @@ async function fillOther(handlers, account, payees, groups) {
async function createBudget(accounts, payees, groups) {
const primaryAccount = accounts.find(a => (a.name = 'Bank of America'));
const earliestDate = (
await db.first(
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
await db.first<db.DbViewTransaction>(
`SELECT t.date FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
WHERE a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`,
)
).date;
const earliestPrimaryDate = (
await db.first(
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
await db.first<db.DbViewTransaction>(
`SELECT t.date FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
WHERE a.id = ? AND a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`,
[primaryAccount.id],
)
Expand Down
2 changes: 1 addition & 1 deletion packages/loot-core/src/server/accounts/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import * as db from '../db';

export async function findOrCreateBank(institution, requisitionId) {
const bank = await db.first(
const bank = await db.first<Pick<db.DbBank, 'id' | 'bank_id'>>(
'SELECT id, bank_id, name FROM banks WHERE bank_id = ?',
[requisitionId],
);
Expand Down
6 changes: 3 additions & 3 deletions packages/loot-core/src/server/accounts/payees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as db from '../db';
export async function createPayee(description) {
// Check to make sure no payee already exists with exactly the same
// name
const row = await db.first(
const row = await db.first<Pick<db.DbPayee, 'id'>>(
`SELECT id FROM payees WHERE UNICODE_LOWER(name) = ? AND tombstone = 0`,
[description.toLowerCase()],
);
Expand All @@ -17,14 +17,14 @@ export async function createPayee(description) {
}

export async function getStartingBalancePayee() {
let category = await db.first(`
let category = await db.first<db.DbCategory>(`
SELECT * FROM categories
WHERE is_income = 1 AND
LOWER(name) = 'starting balances' AND
tombstone = 0
`);
if (category === null) {
category = await db.first(
category = await db.first<db.DbCategory>(
'SELECT * FROM categories WHERE is_income = 1 AND tombstone = 0',
);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/loot-core/src/server/accounts/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ beforeEach(async () => {
});

function getAllTransactions() {
return db.all(
return db.all<
db.DbViewTransactionInternal & { payee_name: db.DbPayee['name'] }
>(
`SELECT t.*, p.name as payee_name
FROM v_transactions_internal t
LEFT JOIN payees p ON p.id = t.payee
Expand Down
46 changes: 38 additions & 8 deletions packages/loot-core/src/server/accounts/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ export async function reconcileTransactions(
}

if (existing.is_parent && existing.cleared !== updates.cleared) {
const children = await db.all(
const children = await db.all<db.DbViewTransaction>(
'SELECT id FROM v_transactions WHERE parent_id = ?',
[existing.id],
);
Expand Down Expand Up @@ -527,7 +527,7 @@ export async function matchTransactions(
);

// The first pass runs the rules, and preps data for fuzzy matching
const accounts: AccountEntity[] = await db.getAccounts();
const accounts: db.DbAccount[] = await db.getAccounts();
const accountsMap = new Map(accounts.map(account => [account.id, account]));

const transactionsStep1 = [];
Expand All @@ -546,7 +546,7 @@ export async function matchTransactions(
// is the highest fidelity match and should always be attempted
// first.
if (trans.imported_id) {
match = await db.first(
match = await db.first<db.DbViewTransaction>(
'SELECT * FROM v_transactions WHERE imported_id = ? AND account = ?',
[trans.imported_id, acctId],
);
Expand All @@ -567,7 +567,22 @@ export async function matchTransactions(
// strictIdChecking has the added behaviour of only matching on transactions with no import ID
// if the transaction being imported has an import ID.
if (strictIdChecking) {
fuzzyDataset = await db.all(
fuzzyDataset = await db.all<
Pick<
db.DbViewTransaction,
| 'id'
| 'is_parent'
| 'date'
| 'imported_id'
| 'payee'
| 'imported_payee'
| 'category'
| 'notes'
| 'reconciled'
| 'cleared'
| 'amount'
>
>(
`SELECT id, is_parent, date, imported_id, payee, imported_payee, category, notes, reconciled, cleared, amount
FROM v_transactions
WHERE
Expand All @@ -584,7 +599,22 @@ export async function matchTransactions(
],
);
} else {
fuzzyDataset = await db.all(
fuzzyDataset = await db.all<
Pick<
db.DbViewTransaction,
| 'id'
| 'is_parent'
| 'date'
| 'imported_id'
| 'payee'
| 'imported_payee'
| 'category'
| 'notes'
| 'reconciled'
| 'cleared'
| 'amount'
>
>(
`SELECT id, is_parent, date, imported_id, payee, imported_payee, category, notes, reconciled, cleared, amount
FROM v_transactions
WHERE date >= ? AND date <= ? AND amount = ? AND account = ?`,
Expand Down Expand Up @@ -680,7 +710,7 @@ export async function addTransactions(
{ rawPayeeName: true },
);

const accounts: AccountEntity[] = await db.getAccounts();
const accounts: db.DbAccount[] = await db.getAccounts();
const accountsMap = new Map(accounts.map(account => [account.id, account]));

for (const { trans: originalTrans, subtransactions } of normalized) {
Expand Down Expand Up @@ -816,7 +846,7 @@ export async function syncAccount(
acctId: string,
bankId: string,
) {
const acctRow = await db.select('accounts', id);
const acctRow = await db.select<db.DbAccount>('accounts', id);

const syncStartDate = await getAccountSyncStartDate(id);
const oldestTransaction = await getAccountOldestTransaction(id);
Expand Down Expand Up @@ -863,7 +893,7 @@ export async function SimpleFinBatchSync(
const account = accounts[i];
const download = res[account.accountId];

const acctRow = await db.select('accounts', account.id);
const acctRow = await db.select<db.DbAccount>('accounts', account.id);
const oldestTransaction = await getAccountOldestTransaction(account.id);
const newAccount = oldestTransaction == null;

Expand Down
32 changes: 19 additions & 13 deletions packages/loot-core/src/server/accounts/transaction-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('Transaction rules', () => {
conditions: [],
actions: [],
});
expect((await db.all('SELECT * FROM rules')).length).toBe(1);
expect((await db.all<db.DbRule>('SELECT * FROM rules')).length).toBe(1);
// Make sure it was projected
expect(getRules().length).toBe(1);

Expand All @@ -110,7 +110,7 @@ describe('Transaction rules', () => {
{ op: 'set', field: 'category', value: 'food' },
],
});
expect((await db.all('SELECT * FROM rules')).length).toBe(2);
expect((await db.all<db.DbRule>('SELECT * FROM rules')).length).toBe(2);
expect(getRules().length).toBe(2);

const spy = jest.spyOn(console, 'warn').mockImplementation();
Expand All @@ -119,7 +119,7 @@ describe('Transaction rules', () => {
// that will validate the input)
await db.insertWithUUID('rules', { conditions: '{', actions: '}' });
// It will be in the database
expect((await db.all('SELECT * FROM rules')).length).toBe(3);
expect((await db.all<db.DbRule>('SELECT * FROM rules')).length).toBe(3);
// But it will be ignored
expect(getRules().length).toBe(2);

Expand Down Expand Up @@ -943,11 +943,14 @@ describe('Learning categories', () => {

// Internally, it should still be stored with the internal names
// so that it's backwards compatible
const rawRule = await db.first('SELECT * FROM rules');
rawRule.conditions = JSON.parse(rawRule.conditions);
rawRule.actions = JSON.parse(rawRule.actions);
expect(rawRule.conditions[0].field).toBe('imported_description');
expect(rawRule.actions[0].field).toBe('description');
const rawRule = await db.first<db.DbRule>('SELECT * FROM rules');
const parsedRule = {
...rawRule,
conditions: JSON.parse(rawRule.conditions),
actions: JSON.parse(rawRule.actions),
};
expect(parsedRule.conditions[0].field).toBe('imported_description');
expect(parsedRule.actions[0].field).toBe('description');

await loadRules();

Expand All @@ -973,11 +976,14 @@ describe('Learning categories', () => {
// This rule internally has been stored with the public names.
// Making this work now allows us to switch to it by default in
// the future
const rawRule = await db.first('SELECT * FROM rules');
rawRule.conditions = JSON.parse(rawRule.conditions);
rawRule.actions = JSON.parse(rawRule.actions);
expect(rawRule.conditions[0].field).toBe('imported_payee');
expect(rawRule.actions[0].field).toBe('payee');
const rawRule = await db.first<db.DbRule>('SELECT * FROM rules');
const parsedRule = {
...rawRule,
conditions: JSON.parse(rawRule.conditions),
actions: JSON.parse(rawRule.actions),
};
expect(parsedRule.conditions[0].field).toBe('imported_payee');
expect(parsedRule.actions[0].field).toBe('payee');

const [rule] = getRules();
expect(rule.conditions[0].field).toBe('imported_payee');
Expand Down
26 changes: 12 additions & 14 deletions packages/loot-core/src/server/accounts/transaction-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
type TransactionEntity,
type RuleActionEntity,
type RuleEntity,
AccountEntity,
} from '../../types/models';
import { schemaConfig } from '../aql';
import * as db from '../db';
Expand Down Expand Up @@ -175,7 +174,7 @@ export function makeRule(data) {
export async function loadRules() {
resetState();

const rules = await db.all(`
const rules = await db.all<db.DbRule>(`
SELECT * FROM rules
WHERE conditions IS NOT NULL AND actions IS NOT NULL AND tombstone = 0
`);
Expand Down Expand Up @@ -219,9 +218,10 @@ export async function updateRule(rule) {
}

export async function deleteRule(id: string) {
const schedule = await db.first('SELECT id FROM schedules WHERE rule = ?', [
id,
]);
const schedule = await db.first<Pick<db.DbSchedule, 'id'>>(
'SELECT id FROM schedules WHERE rule = ?',
[id],
);

if (schedule) {
return false;
Expand Down Expand Up @@ -277,7 +277,7 @@ function onApplySync(oldValues, newValues) {
// Runner
export async function runRules(
trans,
accounts: Map<string, AccountEntity> | null = null,
accounts: Map<string, db.DbAccount> | null = null,
) {
let accountsMap = null;
if (accounts === null) {
Expand Down Expand Up @@ -627,13 +627,11 @@ export async function applyActions(
return null;
}

const accounts: AccountEntity[] = await db.getAccounts();
const accounts: db.DbAccount[] = await db.getAccounts();
const accountsMap = new Map(accounts.map(account => [account.id, account]));
const transactionsForRules = await Promise.all(
transactions.map(transactions =>
prepareTransactionForRules(
transactions,
new Map(accounts.map(account => [account.id, account])),
),
prepareTransactionForRules(transactions, accountsMap),
),
);

Expand Down Expand Up @@ -798,7 +796,7 @@ export async function updateCategoryRules(transactions) {

// Also look 180 days in the future to get any future transactions
// (this might change when we think about scheduled transactions)
const register: TransactionEntity[] = await db.all(
const register = await db.all<db.DbViewTransaction>(
`SELECT t.* FROM v_transactions t
LEFT JOIN accounts a ON a.id = t.account
LEFT JOIN payees p ON p.id = t.payee
Expand Down Expand Up @@ -866,12 +864,12 @@ export async function updateCategoryRules(transactions) {

export type TransactionForRules = TransactionEntity & {
payee_name?: string;
_account?: AccountEntity;
_account?: db.DbAccount;
};

export async function prepareTransactionForRules(
trans: TransactionEntity,
accounts: Map<string, AccountEntity> | null = null,
accounts: Map<string, db.DbAccount> | null = null,
): Promise<TransactionForRules> {
const r: TransactionForRules = { ...trans };
if (trans.payee) {
Expand Down
19 changes: 13 additions & 6 deletions packages/loot-core/src/server/accounts/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as transfer from './transfer';

async function idsWithChildren(ids: string[]) {
const whereIds = whereIn(ids, 'parent_id');
const rows = await db.all(
const rows = await db.all<db.DbViewTransactionInternal>(
`SELECT id FROM v_transactions_internal WHERE ${whereIds}`,
);
const set = new Set(ids);
Expand All @@ -22,13 +22,18 @@ async function idsWithChildren(ids: string[]) {
}

async function getTransactionsByIds(
ids: string[],
): Promise<TransactionEntity[]> {
ids: Array<db.DbTransaction['id']>,
): Promise<db.DbViewTransactionInternal[]> {
// TODO: convert to whereIn
//
// or better yet, use ActualQL
return incrFetch(
(query, params) => db.selectWithSchema('transactions', query, params),
return incrFetch<db.DbViewTransactionInternal>(
(sql, params) =>
db.selectWithSchema<db.DbViewTransactionInternal>(
'transactions',
sql,
params,
),
ids,
// eslint-disable-next-line rulesdir/typography
id => `id = '${id}'`,
Expand Down Expand Up @@ -56,7 +61,9 @@ export async function batchUpdateTransactions({
: [];

const oldPayees = new Set<PayeeEntity['id']>();
const accounts = await db.all('SELECT * FROM accounts WHERE tombstone = 0');
const accounts = await db.all<db.DbAccount>(
'SELECT * FROM accounts WHERE tombstone = 0',
);

// We need to get all the payees of updated transactions _before_
// making changes
Expand Down
10 changes: 5 additions & 5 deletions packages/loot-core/src/server/accounts/transfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
beforeEach(global.emptyDatabase());

function getAllTransactions() {
return db.all(
return db.all<db.DbViewTransaction & { payee_name: db.DbPayee['name'] }>(
`SELECT t.*, p.name as payee_name
FROM v_transactions t
LEFT JOIN payees p ON p.id = t.payee
Expand Down Expand Up @@ -61,10 +61,10 @@

const differ = expectSnapshotWithDiffer(await getAllTransactions());

const transferTwo = await db.first(
const transferTwo = await db.first<db.DbPayee>(
"SELECT * FROM payees WHERE transfer_acct = 'two'",
);
const transferThree = await db.first(
const transferThree = await db.first<db.DbPayee>(
"SELECT * FROM payees WHERE transfer_acct = 'three'",
);

Expand All @@ -79,7 +79,7 @@
differ.expectToMatchDiff(await getAllTransactions());

// Fill the transaction out
transaction = await db.getTransaction(transaction.id);

Check failure on line 82 in packages/loot-core/src/server/accounts/transfer.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type 'DbViewTransactionInternal' is not assignable to type 'Transaction'.
expect(transaction.transfer_id).toBeDefined();

transaction = {
Expand Down Expand Up @@ -108,7 +108,7 @@
differ.expectToMatchDiff(await getAllTransactions());

// Make sure it's not a linked transaction anymore
transaction = await db.getTransaction(transaction.id);

Check failure on line 111 in packages/loot-core/src/server/accounts/transfer.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type 'DbViewTransactionInternal' is not assignable to type 'Transaction'.
expect(transaction.transfer_id).toBeNull();

// Re-transfer it
Expand All @@ -120,7 +120,7 @@
await transfer.onUpdate(transaction);
differ.expectToMatchDiff(await getAllTransactions());

transaction = await db.getTransaction(transaction.id);

Check failure on line 123 in packages/loot-core/src/server/accounts/transfer.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type 'DbViewTransactionInternal' is not assignable to type 'Transaction'.
expect(transaction.transfer_id).toBeDefined();

await db.deleteTransaction(transaction);
Expand All @@ -131,10 +131,10 @@
test('transfers are properly de-categorized', async () => {
await prepareDatabase();

const transferTwo = await db.first(
const transferTwo = await db.first<db.DbPayee>(
"SELECT * FROM payees WHERE transfer_acct = 'two'",
);
const transferThree = await db.first(
const transferThree = await db.first<db.DbPayee>(
"SELECT * FROM payees WHERE transfer_acct = 'three'",
);

Expand All @@ -150,7 +150,7 @@

const differ = expectSnapshotWithDiffer(await getAllTransactions());

transaction = {

Check failure on line 153 in packages/loot-core/src/server/accounts/transfer.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type '{ payee: string; notes: string; id: db.DbTransaction["id"]; is_parent: db.DbTransaction["isParent"]; is_child: db.DbTransaction["isChild"]; ... 14 more ...; reconciled: db.DbTransaction["reconciled"]; }' is not assignable to type 'Transaction'.
...(await db.getTransaction(transaction.id)),
payee: transferThree.id,
notes: 'hi',
Expand All @@ -159,7 +159,7 @@
await transfer.onUpdate(transaction);
differ.expectToMatchDiff(await getAllTransactions());

transaction = {

Check failure on line 162 in packages/loot-core/src/server/accounts/transfer.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type '{ payee: string; id: db.DbTransaction["id"]; is_parent: db.DbTransaction["isParent"]; is_child: db.DbTransaction["isChild"]; ... 15 more ...; reconciled: db.DbTransaction["reconciled"]; }' is not assignable to type 'Transaction'.
...(await db.getTransaction(transaction.id)),
payee: transferTwo.id,
};
Expand Down
Loading
Loading