mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 11:42:54 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c69a4937 | ||
|
|
8275ad90b2 | ||
|
|
43331b957b |
@@ -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
|
||||
|
||||
@@ -557,7 +557,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],
|
||||
);
|
||||
@@ -670,7 +670,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
|
||||
@@ -687,7 +702,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 = ?`,
|
||||
|
||||
@@ -53,7 +53,7 @@ function withMutation<Params extends Array<unknown>, ReturnType>(
|
||||
const latestTimestamp = getClock().timestamp.toString();
|
||||
const result = await handler(...args);
|
||||
|
||||
const rows = await db.all(
|
||||
const rows = await db.all<Pick<db.DbCrdtMessage, 'dataset'>>(
|
||||
'SELECT DISTINCT dataset FROM messages_crdt WHERE timestamp > ?',
|
||||
[latestTimestamp],
|
||||
);
|
||||
@@ -355,7 +355,8 @@ handlers['api/budget-month'] = async function ({ month }) {
|
||||
checkFileOpen();
|
||||
await validateMonth(month);
|
||||
|
||||
const groups = await db.getCategoriesGrouped();
|
||||
const grouped = await db.getCategoriesGrouped();
|
||||
const groups = categoryGroupModel.fromDbArray(grouped);
|
||||
const sheetName = monthUtils.sheetForMonth(month);
|
||||
|
||||
function value(name) {
|
||||
@@ -556,7 +557,7 @@ handlers['api/transaction-delete'] = withMutation(async function ({ id }) {
|
||||
|
||||
handlers['api/accounts-get'] = async function () {
|
||||
checkFileOpen();
|
||||
const accounts = await db.getAccounts();
|
||||
const accounts = accountModel.fromDbArray(await db.getAccounts());
|
||||
return accounts.map(account => accountModel.toExternal(account));
|
||||
};
|
||||
|
||||
|
||||
@@ -261,7 +261,9 @@ describe('runQuery', () => {
|
||||
it('fetches all data required for $oneof', async () => {
|
||||
await insertTransactions();
|
||||
|
||||
const rows = await db.all('SELECT id FROM transactions WHERE amount < -50');
|
||||
const rows = await db.all<Pick<db.DbTransaction, 'id'>>(
|
||||
'SELECT id FROM transactions WHERE amount < -50',
|
||||
);
|
||||
const ids = rows.slice(0, 3).map(row => row.id);
|
||||
ids.sort();
|
||||
|
||||
|
||||
@@ -110,8 +110,8 @@ async function execTransactionsGrouped(
|
||||
return execQuery(queryState, state, s, params, outputTypes);
|
||||
}
|
||||
|
||||
let rows;
|
||||
let matched = null;
|
||||
let rows: Array<{ group_id: db.DbTransaction['id']; matched: string }>;
|
||||
let matched: Set<db.DbTransaction['id']> = null;
|
||||
|
||||
if (isHappyPathQuery(queryState)) {
|
||||
// This is just an optimization - we can just filter out children
|
||||
@@ -171,7 +171,9 @@ async function execTransactionsGrouped(
|
||||
${sql.orderBy}
|
||||
`;
|
||||
|
||||
const allRows = await db.all(finalSql);
|
||||
const allRows = await db.all<
|
||||
db.DbTransaction & { _parent_id: db.DbTransaction['id'] }
|
||||
>(finalSql);
|
||||
|
||||
// Group the parents and children up
|
||||
const { parents, children } = allRows.reduce(
|
||||
|
||||
@@ -9,29 +9,59 @@ beforeEach(global.emptyDatabase());
|
||||
|
||||
describe('schema', () => {
|
||||
test('never returns transactions without a date', async () => {
|
||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(0);
|
||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
|
||||
).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
|
||||
.length,
|
||||
).toBe(0);
|
||||
await db.runQuery('INSERT INTO transactions (acct) VALUES (?)', ['foo']);
|
||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(1);
|
||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
|
||||
).toBe(1);
|
||||
expect(
|
||||
(await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
|
||||
.length,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('never returns transactions without an account', async () => {
|
||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(0);
|
||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
|
||||
).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
|
||||
.length,
|
||||
).toBe(0);
|
||||
await db.runQuery('INSERT INTO transactions (date) VALUES (?)', [20200101]);
|
||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(1);
|
||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
|
||||
).toBe(1);
|
||||
expect(
|
||||
(await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
|
||||
.length,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('never returns child transactions without a parent', async () => {
|
||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(0);
|
||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
|
||||
).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
|
||||
.length,
|
||||
).toBe(0);
|
||||
await db.runQuery(
|
||||
'INSERT INTO transactions (date, acct, isChild) VALUES (?, ?, ?)',
|
||||
[20200101, 'foo', 1],
|
||||
);
|
||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(1);
|
||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||
expect(
|
||||
(await db.all<db.DbTransaction>('SELECT * FROM transactions')).length,
|
||||
).toBe(1);
|
||||
expect(
|
||||
(await db.all<db.DbViewTransaction>('SELECT * FROM v_transactions'))
|
||||
.length,
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,8 +61,14 @@ type BudgetData = {
|
||||
amount: number;
|
||||
};
|
||||
|
||||
function getBudgetData(table: string, month: string): Promise<BudgetData[]> {
|
||||
return db.all(
|
||||
function getBudgetData(
|
||||
table: BudgetTable,
|
||||
month: string,
|
||||
): Promise<BudgetData[]> {
|
||||
return db.all<
|
||||
(db.DbZeroBudget | db.DbReflectBudget) &
|
||||
Pick<db.DbViewCategory, 'is_income'>
|
||||
>(
|
||||
`
|
||||
SELECT b.*, c.is_income FROM v_categories c
|
||||
LEFT JOIN ${table} b ON b.category = c.id
|
||||
@@ -233,7 +239,7 @@ export async function copySinglePreviousMonth({
|
||||
}
|
||||
|
||||
export async function setZero({ month }: { month: string }): Promise<void> {
|
||||
const categories = await db.all(
|
||||
const categories = await db.all<db.DbViewCategory>(
|
||||
'SELECT * FROM v_categories WHERE tombstone = 0',
|
||||
);
|
||||
|
||||
@@ -252,7 +258,7 @@ export async function set3MonthAvg({
|
||||
}: {
|
||||
month: string;
|
||||
}): Promise<void> {
|
||||
const categories = await db.all(
|
||||
const categories = await db.all<db.DbViewCategory>(
|
||||
'SELECT * FROM v_categories WHERE tombstone = 0',
|
||||
);
|
||||
|
||||
@@ -295,7 +301,7 @@ export async function set12MonthAvg({
|
||||
}: {
|
||||
month: string;
|
||||
}): Promise<void> {
|
||||
const categories = await db.all(
|
||||
const categories = await db.all<db.DbViewCategory>(
|
||||
'SELECT * FROM v_categories WHERE tombstone = 0',
|
||||
);
|
||||
|
||||
@@ -314,7 +320,7 @@ export async function set6MonthAvg({
|
||||
}: {
|
||||
month: string;
|
||||
}): Promise<void> {
|
||||
const categories = await db.all(
|
||||
const categories = await db.all<db.DbViewCategory>(
|
||||
'SELECT * FROM v_categories WHERE tombstone = 0',
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { getChangedValues } from '../../shared/util';
|
||||
import * as db from '../db';
|
||||
import { categoryGroupModel } from '../models';
|
||||
import * as sheet from '../sheet';
|
||||
import { resolveName } from '../spreadsheet/util';
|
||||
|
||||
@@ -391,7 +392,8 @@ export async function doTransfer(categoryIds, transferId) {
|
||||
|
||||
export async function createBudget(months) {
|
||||
const categories = await db.getCategories();
|
||||
const groups = await db.getCategoriesGrouped();
|
||||
const grouped = await db.getCategoriesGrouped();
|
||||
const groups = categoryGroupModel.fromDbArray(grouped);
|
||||
|
||||
sheet.startTransaction();
|
||||
const meta = sheet.get().meta();
|
||||
|
||||
@@ -132,7 +132,7 @@ async function processCleanup(month: string): Promise<Notification> {
|
||||
const db_month = parseInt(month.replace('-', ''));
|
||||
|
||||
const category_templates = await getCategoryTemplates();
|
||||
const categories = await db.all(
|
||||
const categories = await db.all<db.DbViewCategory>(
|
||||
'SELECT * FROM v_categories WHERE tombstone = 0',
|
||||
);
|
||||
const sheetName = monthUtils.sheetForMonth(month);
|
||||
@@ -369,7 +369,7 @@ const TEMPLATE_PREFIX = '#cleanup ';
|
||||
async function getCategoryTemplates() {
|
||||
const templates = {};
|
||||
|
||||
const notes = await db.all(
|
||||
const notes = await db.all<db.DbNote>(
|
||||
`SELECT * FROM notes WHERE lower(note) like '%${TEMPLATE_PREFIX}%'`,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { Notification } from '../../client/state-types/notifications';
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { CategoryEntity } from '../../types/models';
|
||||
import * as db from '../db';
|
||||
import { batchMessages } from '../sync';
|
||||
|
||||
@@ -26,7 +25,7 @@ export async function overwriteTemplate({ month }): Promise<Notification> {
|
||||
export async function applyMultipleCategoryTemplates({ month, categoryIds }) {
|
||||
const placeholders = categoryIds.map(() => '?').join(', ');
|
||||
const query = `SELECT * FROM v_categories WHERE id IN (${placeholders})`;
|
||||
const categories = await db.all(query, categoryIds);
|
||||
const categories = await db.all<db.DbViewCategory>(query, categoryIds);
|
||||
await storeTemplates();
|
||||
const categoryTemplates = await getTemplates(categories);
|
||||
const ret = await processTemplate(month, true, categoryTemplates, categories);
|
||||
@@ -34,9 +33,10 @@ export async function applyMultipleCategoryTemplates({ month, categoryIds }) {
|
||||
}
|
||||
|
||||
export async function applySingleCategoryTemplate({ month, category }) {
|
||||
const categories = await db.all(`SELECT * FROM v_categories WHERE id = ?`, [
|
||||
category,
|
||||
]);
|
||||
const categories = await db.all<db.DbViewCategory>(
|
||||
`SELECT * FROM v_categories WHERE id = ?`,
|
||||
[category],
|
||||
);
|
||||
await storeTemplates();
|
||||
const categoryTemplates = await getTemplates(categories[0]);
|
||||
const ret = await processTemplate(month, true, categoryTemplates, categories);
|
||||
@@ -47,8 +47,8 @@ export function runCheckTemplates() {
|
||||
return checkTemplates();
|
||||
}
|
||||
|
||||
async function getCategories(): Promise<CategoryEntity[]> {
|
||||
return await db.all(
|
||||
async function getCategories(): Promise<db.DbCategory[]> {
|
||||
return await db.all<db.DbCategory>(
|
||||
`
|
||||
SELECT categories.* FROM categories
|
||||
INNER JOIN category_groups on categories.cat_group = category_groups.id
|
||||
@@ -60,7 +60,7 @@ async function getCategories(): Promise<CategoryEntity[]> {
|
||||
|
||||
async function getTemplates(category) {
|
||||
//retrieves template definitions from the database
|
||||
const goalDef = await db.all(
|
||||
const goalDef = await db.all<db.DbCategory>(
|
||||
'SELECT * FROM categories WHERE goal_def IS NOT NULL',
|
||||
);
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ export type CategoryWithTemplateNote = {
|
||||
export async function getCategoriesWithTemplateNotes(): Promise<
|
||||
CategoryWithTemplateNote[]
|
||||
> {
|
||||
return await db.all(
|
||||
return await db.all<
|
||||
Pick<db.DbNote, 'note'> & Pick<db.DbCategory, 'id' | 'name'>
|
||||
>(
|
||||
`
|
||||
SELECT c.id AS id, c.name as name, n.note AS note
|
||||
FROM notes n
|
||||
@@ -42,7 +44,18 @@ export async function getCategoriesWithTemplateNotes(): Promise<
|
||||
}
|
||||
|
||||
export async function getActiveSchedules(): Promise<DbSchedule[]> {
|
||||
return await db.all(
|
||||
return await db.all<
|
||||
Pick<
|
||||
DbSchedule,
|
||||
| 'id'
|
||||
| 'rule'
|
||||
| 'active'
|
||||
| 'completed'
|
||||
| 'posts_transaction'
|
||||
| 'tombstone'
|
||||
| 'name'
|
||||
>
|
||||
>(
|
||||
'SELECT id, rule, active, completed, posts_transaction, tombstone, name from schedules WHERE name NOT NULL AND tombstone = 0',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as fs from '../../platform/server/fs';
|
||||
import { DEFAULT_DASHBOARD_STATE } from '../../shared/dashboard';
|
||||
import { q } from '../../shared/query';
|
||||
import {
|
||||
type CustomReportEntity,
|
||||
type ExportImportDashboard,
|
||||
type ExportImportDashboardWidget,
|
||||
type ExportImportCustomReportWidget,
|
||||
@@ -178,7 +177,7 @@ async function importDashboard({ filepath }: { filepath: string }) {
|
||||
|
||||
exportModel.validate(parsedContent);
|
||||
|
||||
const customReportIds: CustomReportEntity[] = await db.all(
|
||||
const customReportIds = await db.all<Pick<db.DbCustomReport, 'id'>>(
|
||||
'SELECT id from custom_reports',
|
||||
);
|
||||
const customReportIdSet = new Set(customReportIds.map(({ id }) => id));
|
||||
|
||||
@@ -15,7 +15,6 @@ import * as fs from '../../platform/server/fs';
|
||||
import * as sqlite from '../../platform/server/sqlite';
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { groupById } from '../../shared/util';
|
||||
import { CategoryEntity, CategoryGroupEntity } from '../../types/models';
|
||||
import { WithRequired } from '../../types/util';
|
||||
import {
|
||||
schema,
|
||||
@@ -36,12 +35,16 @@ import { sendMessages, batchMessages } from '../sync';
|
||||
import { shoveSortOrders, SORT_INCREMENT } from './sort';
|
||||
import {
|
||||
DbAccount,
|
||||
DbBank,
|
||||
DbCategory,
|
||||
DbCategoryGroup,
|
||||
DbCategoryMapping,
|
||||
DbClockMessage,
|
||||
DbPayee,
|
||||
DbPayeeMapping,
|
||||
DbTransaction,
|
||||
DbViewTransaction,
|
||||
DbViewTransactionInternalAlive,
|
||||
} from './types';
|
||||
|
||||
export * from './types';
|
||||
@@ -161,11 +164,8 @@ export function asyncTransaction(fn: () => Promise<void>) {
|
||||
// This function is marked as async because `runQuery` is no longer
|
||||
// async. We return a promise here until we've audited all the code to
|
||||
// make sure nothing calls `.then` on this.
|
||||
export async function all(sql, params?: (string | number)[]) {
|
||||
// TODO: In the next phase, we will make this function generic
|
||||
// and pass the type of the return type to `runQuery`.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return runQuery(sql, params, true) as any[];
|
||||
export async function all<T>(sql, params?: (string | number)[]) {
|
||||
return runQuery<T>(sql, params, true);
|
||||
}
|
||||
|
||||
export async function first<T>(sql, params?: (string | number)[]) {
|
||||
@@ -265,7 +265,7 @@ export async function delete_(table, id) {
|
||||
}
|
||||
|
||||
export async function deleteAll(table: string) {
|
||||
const rows: Array<{ id: string }> = await all(`
|
||||
const rows = await all<{ id: string }>(`
|
||||
SELECT id FROM ${table} WHERE tombstone = 0
|
||||
`);
|
||||
await Promise.all(rows.map(({ id }) => delete_(table, id)));
|
||||
@@ -306,19 +306,25 @@ export function updateWithSchema(table, fields) {
|
||||
// Data-specific functions. Ideally this would be split up into
|
||||
// different files
|
||||
|
||||
// TODO: Fix return type. This should returns a DbCategory[].
|
||||
export async function getCategories(
|
||||
ids?: Array<DbCategory['id']>,
|
||||
): Promise<CategoryEntity[]> {
|
||||
): Promise<DbCategory[]> {
|
||||
const whereIn = ids ? `c.id IN (${toSqlQueryParameters(ids)}) AND` : '';
|
||||
const query = `SELECT c.* FROM categories c WHERE ${whereIn} c.tombstone = 0 ORDER BY c.sort_order, c.id`;
|
||||
return ids ? await all(query, [...ids]) : await all(query);
|
||||
return ids
|
||||
? await all<DbCategory>(query, [...ids])
|
||||
: await all<DbCategory>(query);
|
||||
}
|
||||
|
||||
// TODO: Fix return type. This should returns a [DbCategoryGroup, ...DbCategory].
|
||||
/**
|
||||
* Get all categories grouped by their category group.
|
||||
* @param ids The IDs of the category groups to get.
|
||||
* @returns The categories grouped by their category group.
|
||||
* The first element of each tuple is the category group, and the rest are the categories that belong to that group.
|
||||
*/
|
||||
export async function getCategoriesGrouped(
|
||||
ids?: Array<DbCategoryGroup['id']>,
|
||||
): Promise<Array<CategoryGroupEntity>> {
|
||||
): Promise<Array<[DbCategoryGroup, ...DbCategory[]]>> {
|
||||
const categoryGroupWhereIn = ids
|
||||
? `cg.id IN (${toSqlQueryParameters(ids)}) AND`
|
||||
: '';
|
||||
@@ -332,18 +338,15 @@ export async function getCategoriesGrouped(
|
||||
ORDER BY c.sort_order, c.id`;
|
||||
|
||||
const groups = ids
|
||||
? await all(categoryGroupQuery, [...ids])
|
||||
: await all(categoryGroupQuery);
|
||||
? await all<DbCategoryGroup>(categoryGroupQuery, [...ids])
|
||||
: await all<DbCategoryGroup>(categoryGroupQuery);
|
||||
|
||||
const categories = ids
|
||||
? await all(categoryQuery, [...ids])
|
||||
: await all(categoryQuery);
|
||||
? await all<DbCategory>(categoryQuery, [...ids])
|
||||
: await all<DbCategory>(categoryQuery);
|
||||
|
||||
return groups.map(group => {
|
||||
return {
|
||||
...group,
|
||||
categories: categories.filter(c => c.cat_group === group.id),
|
||||
};
|
||||
return [group, ...categories.filter(c => c.cat_group === group.id)];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -379,7 +382,7 @@ export function updateCategoryGroup(group) {
|
||||
}
|
||||
|
||||
export async function moveCategoryGroup(id, targetId) {
|
||||
const groups = await all(
|
||||
const groups = await all<Pick<DbCategoryGroup, 'id' | 'sort_order'>>(
|
||||
`SELECT id, sort_order FROM category_groups WHERE tombstone = 0 ORDER BY sort_order, id`,
|
||||
);
|
||||
|
||||
@@ -391,9 +394,10 @@ export async function moveCategoryGroup(id, targetId) {
|
||||
}
|
||||
|
||||
export async function deleteCategoryGroup(group, transferId?: string) {
|
||||
const categories = await all('SELECT * FROM categories WHERE cat_group = ?', [
|
||||
group.id,
|
||||
]);
|
||||
const categories = await all<DbCategory>(
|
||||
'SELECT * FROM categories WHERE cat_group = ?',
|
||||
[group.id],
|
||||
);
|
||||
|
||||
// Delete all the categories within a group
|
||||
await Promise.all(categories.map(cat => deleteCategory(cat, transferId)));
|
||||
@@ -427,7 +431,7 @@ export async function insertCategory(
|
||||
} else {
|
||||
// Unfortunately since we insert at the beginning, we need to shove
|
||||
// the sort orders to make sure there's room for it
|
||||
const categories = await all(
|
||||
const categories = await all<Pick<DbCategory, 'id' | 'sort_order'>>(
|
||||
`SELECT id, sort_order FROM categories WHERE cat_group = ? AND tombstone = 0 ORDER BY sort_order, id`,
|
||||
[category.cat_group],
|
||||
);
|
||||
@@ -469,7 +473,7 @@ export async function moveCategory(
|
||||
throw new Error('moveCategory: groupId is required');
|
||||
}
|
||||
|
||||
const categories = await all(
|
||||
const categories = await all<Pick<DbCategory, 'id' | 'sort_order'>>(
|
||||
`SELECT id, sort_order FROM categories WHERE cat_group = ? AND tombstone = 0 ORDER BY sort_order, id`,
|
||||
[groupId],
|
||||
);
|
||||
@@ -489,7 +493,7 @@ export async function deleteCategory(
|
||||
// We need to update all the deleted categories that currently
|
||||
// point to the one we're about to delete so they all are
|
||||
// "forwarded" to the new transferred category.
|
||||
const existingTransfers = await all(
|
||||
const existingTransfers = await all<DbCategoryMapping>(
|
||||
'SELECT * FROM category_mapping WHERE transferId = ?',
|
||||
[category.id],
|
||||
);
|
||||
@@ -559,7 +563,7 @@ export async function mergePayees(
|
||||
ids: Array<DbPayee['id']>,
|
||||
) {
|
||||
// Load in payees so we can check some stuff
|
||||
const dbPayees: DbPayee[] = await all('SELECT * FROM payees');
|
||||
const dbPayees = await all<DbPayee>('SELECT * FROM payees');
|
||||
const payees = groupById(dbPayees);
|
||||
|
||||
// Filter out any transfer payees
|
||||
@@ -571,7 +575,7 @@ export async function mergePayees(
|
||||
await batchMessages(async () => {
|
||||
await Promise.all(
|
||||
ids.map(async id => {
|
||||
const mappings = await all(
|
||||
const mappings = await all<Pick<DbPayeeMapping, 'id'>>(
|
||||
'SELECT id FROM payee_mapping WHERE targetId = ?',
|
||||
[id],
|
||||
);
|
||||
@@ -594,8 +598,8 @@ export async function mergePayees(
|
||||
});
|
||||
}
|
||||
|
||||
export function getPayees(): Promise<DbPayee[]> {
|
||||
return all(`
|
||||
export function getPayees() {
|
||||
return all<DbPayee & { name: DbAccount['name'] | DbPayee['name'] }>(`
|
||||
SELECT p.*, COALESCE(a.name, p.name) AS name FROM payees p
|
||||
LEFT JOIN accounts a ON (p.transfer_acct = a.id AND a.tombstone = 0)
|
||||
WHERE p.tombstone = 0 AND (p.transfer_acct IS NULL OR a.id IS NOT NULL)
|
||||
@@ -603,12 +607,19 @@ export function getPayees(): Promise<DbPayee[]> {
|
||||
`);
|
||||
}
|
||||
|
||||
export function getCommonPayees(): Promise<DbPayee[]> {
|
||||
export function getCommonPayees() {
|
||||
const twelveWeeksAgo = toDateRepr(
|
||||
monthUtils.subWeeks(monthUtils.currentDate(), 12),
|
||||
);
|
||||
const limit = 10;
|
||||
return all(`
|
||||
return all<
|
||||
DbPayee & {
|
||||
common: true;
|
||||
transfer_acct: null;
|
||||
c: number;
|
||||
latest: DbViewTransactionInternalAlive['date'];
|
||||
}
|
||||
>(`
|
||||
SELECT p.id as id, p.name as name, p.favorite as favorite,
|
||||
p.category as category, TRUE as common, NULL as transfer_acct,
|
||||
count(*) as c,
|
||||
@@ -645,12 +656,12 @@ const orphanedPayeesQuery = `
|
||||
`;
|
||||
/* eslint-enable rulesdir/typography */
|
||||
|
||||
export function syncGetOrphanedPayees(): Promise<Array<Pick<DbPayee, 'id'>>> {
|
||||
return all(orphanedPayeesQuery);
|
||||
export function syncGetOrphanedPayees() {
|
||||
return all<Pick<DbPayee, 'id'>>(orphanedPayeesQuery);
|
||||
}
|
||||
|
||||
export async function getOrphanedPayees(): Promise<Array<DbPayee['id']>> {
|
||||
const rows = await all(orphanedPayeesQuery);
|
||||
export async function getOrphanedPayees() {
|
||||
const rows = await all<Pick<DbPayee, 'id'>>(orphanedPayeesQuery);
|
||||
return rows.map(row => row.id);
|
||||
}
|
||||
|
||||
@@ -662,7 +673,12 @@ export async function getPayeeByName(name: DbPayee['name']) {
|
||||
}
|
||||
|
||||
export function getAccounts() {
|
||||
return all(
|
||||
return all<
|
||||
DbAccount & {
|
||||
bankName: DbBank['name'];
|
||||
bankId: DbBank['id'];
|
||||
}
|
||||
>(
|
||||
`SELECT a.*, b.name as bankName, b.id as bankId FROM accounts a
|
||||
LEFT JOIN banks b ON a.bank = b.id
|
||||
WHERE a.tombstone = 0
|
||||
@@ -671,7 +687,7 @@ export function getAccounts() {
|
||||
}
|
||||
|
||||
export async function insertAccount(account) {
|
||||
const accounts = await all(
|
||||
const accounts = await all<DbAccount>(
|
||||
'SELECT * FROM accounts WHERE offbudget = ? ORDER BY sort_order, name',
|
||||
[account.offbudget ? 1 : 0],
|
||||
);
|
||||
@@ -700,7 +716,7 @@ export async function moveAccount(
|
||||
'SELECT * FROM accounts WHERE id = ?',
|
||||
[id],
|
||||
);
|
||||
let accounts;
|
||||
let accounts: Pick<DbAccount, 'id' | 'sort_order'>[];
|
||||
if (account.closed) {
|
||||
accounts = await all(
|
||||
`SELECT id, sort_order FROM accounts WHERE closed = 1 ORDER BY sort_order, name`,
|
||||
|
||||
@@ -22,12 +22,12 @@ let unlistenSync;
|
||||
export async function loadMappings() {
|
||||
// The mappings are separated into tables specific to the type of
|
||||
// data. But you know, we really could keep a global mapping table.
|
||||
const categories = (await db.all('SELECT * FROM category_mapping')).map(
|
||||
r => [r.id, r.transferId] as const,
|
||||
);
|
||||
const payees = (await db.all('SELECT * FROM payee_mapping')).map(
|
||||
r => [r.id, r.targetId] as const,
|
||||
);
|
||||
const categories = (
|
||||
await db.all<db.DbCategoryMapping>('SELECT * FROM category_mapping')
|
||||
).map(r => [r.id, r.transferId] as const);
|
||||
const payees = (
|
||||
await db.all<db.DbPayeeMapping>('SELECT * FROM payee_mapping')
|
||||
).map(r => [r.id, r.targetId] as const);
|
||||
|
||||
// All ids are unique, so we can just keep a global table of mappings
|
||||
allMappings = new Map(categories.concat(payees));
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('Accounts', () => {
|
||||
date: '2017-01-01',
|
||||
});
|
||||
const differ = expectSnapshotWithDiffer(
|
||||
await db.all('SELECT * FROM transactions'),
|
||||
await db.all<db.DbTransaction>('SELECT * FROM transactions'),
|
||||
);
|
||||
|
||||
let transaction = await db.getTransaction(id);
|
||||
@@ -145,11 +145,15 @@ describe('Accounts', () => {
|
||||
payee: 'transfer-three',
|
||||
date: '2017-01-03',
|
||||
});
|
||||
differ.expectToMatchDiff(await db.all('SELECT * FROM transactions'));
|
||||
differ.expectToMatchDiff(
|
||||
await db.all<db.DbTransaction>('SELECT * FROM transactions'),
|
||||
);
|
||||
|
||||
transaction = await db.getTransaction(id);
|
||||
await runHandler(handlers['transaction-delete'], transaction);
|
||||
differ.expectToMatchDiff(await db.all('SELECT * FROM transactions'));
|
||||
differ.expectToMatchDiff(
|
||||
await db.all<db.DbTransaction>('SELECT * FROM transactions'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import { APIError } from './errors';
|
||||
import { app as filtersApp } from './filters/app';
|
||||
import { handleBudgetImport } from './importers';
|
||||
import { app } from './main-app';
|
||||
import { categoryGroupModel, categoryModel } from './models';
|
||||
import { mutator, runHandler } from './mutators';
|
||||
import { app as notesApp } from './notes/app';
|
||||
import { app as payeesApp } from './payees/app';
|
||||
@@ -104,8 +105,8 @@ handlers['redo'] = mutator(function () {
|
||||
|
||||
handlers['get-categories'] = async function () {
|
||||
return {
|
||||
grouped: await db.getCategoriesGrouped(),
|
||||
list: await db.getCategories(),
|
||||
grouped: categoryGroupModel.fromDbArray(await db.getCategoriesGrouped()),
|
||||
list: categoryModel.fromDbArray(await db.getCategories()),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -114,7 +115,8 @@ handlers['get-budget-bounds'] = async function () {
|
||||
};
|
||||
|
||||
handlers['envelope-budget-month'] = async function ({ month }) {
|
||||
const groups = await db.getCategoriesGrouped();
|
||||
const grouped = await db.getCategoriesGrouped();
|
||||
const groups = categoryGroupModel.fromDbArray(grouped);
|
||||
const sheetName = monthUtils.sheetForMonth(month);
|
||||
|
||||
function value(name) {
|
||||
@@ -166,7 +168,8 @@ handlers['envelope-budget-month'] = async function ({ month }) {
|
||||
};
|
||||
|
||||
handlers['tracking-budget-month'] = async function ({ month }) {
|
||||
const groups = await db.getCategoriesGrouped();
|
||||
const grouped = await db.getCategoriesGrouped();
|
||||
const groups = categoryGroupModel.fromDbArray(grouped);
|
||||
const sheetName = monthUtils.sheetForMonth(month);
|
||||
|
||||
function value(name) {
|
||||
@@ -299,7 +302,8 @@ handlers['category-delete'] = mutator(async function ({ id, transferId }) {
|
||||
});
|
||||
|
||||
handlers['get-category-groups'] = async function () {
|
||||
return await db.getCategoriesGrouped();
|
||||
const grouped = await db.getCategoriesGrouped();
|
||||
return categoryGroupModel.fromDbArray(grouped);
|
||||
};
|
||||
|
||||
handlers['category-group-create'] = mutator(async function ({
|
||||
@@ -336,7 +340,7 @@ handlers['category-group-delete'] = mutator(async function ({
|
||||
transferId,
|
||||
}) {
|
||||
return withUndo(async () => {
|
||||
const groupCategories = await db.all(
|
||||
const groupCategories = await db.all<Pick<db.DbCategory, 'id'>>(
|
||||
'SELECT id FROM categories WHERE cat_group = ? AND tombstone = 0',
|
||||
[id],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { PayeeEntity } from '../types/models';
|
||||
import {
|
||||
AccountEntity,
|
||||
CategoryEntity,
|
||||
CategoryGroupEntity,
|
||||
PayeeEntity,
|
||||
} from '../types/models';
|
||||
|
||||
import {
|
||||
convertForInsert,
|
||||
@@ -7,7 +12,13 @@ import {
|
||||
schema,
|
||||
schemaConfig,
|
||||
} from './aql';
|
||||
import { DbAccount, DbCategory, DbCategoryGroup, DbPayee } from './db';
|
||||
import {
|
||||
DbAccount,
|
||||
DbCategory,
|
||||
DbCategoryGroup,
|
||||
DbPayee,
|
||||
DbTransaction,
|
||||
} from './db';
|
||||
import { ValidationError } from './errors';
|
||||
|
||||
export function requiredFields<T extends object, K extends keyof T>(
|
||||
@@ -63,6 +74,27 @@ export const accountModel = {
|
||||
|
||||
return account as DbAccount;
|
||||
},
|
||||
fromDbArray(accounts: DbAccount[]): AccountEntity[] {
|
||||
return accounts.map(account => accountModel.fromDb(account));
|
||||
},
|
||||
fromDb(account: DbAccount): AccountEntity {
|
||||
return convertFromSelect(
|
||||
schema,
|
||||
schemaConfig,
|
||||
'accounts',
|
||||
account,
|
||||
) as AccountEntity;
|
||||
},
|
||||
toDb(
|
||||
account: AccountEntity,
|
||||
{ update }: { update?: boolean } = {},
|
||||
): DbAccount {
|
||||
return (
|
||||
update
|
||||
? convertForUpdate(schema, schemaConfig, 'accounts', account)
|
||||
: convertForInsert(schema, schemaConfig, 'accounts', account)
|
||||
) as DbAccount;
|
||||
},
|
||||
};
|
||||
|
||||
export const categoryModel = {
|
||||
@@ -80,6 +112,27 @@ export const categoryModel = {
|
||||
const { sort_order, ...rest } = category;
|
||||
return { ...rest, hidden: rest.hidden ? 1 : 0 } as DbCategory;
|
||||
},
|
||||
fromDbArray(categories: DbCategory[]): CategoryEntity[] {
|
||||
return categories.map(category => categoryModel.fromDb(category));
|
||||
},
|
||||
fromDb(category: DbCategory): CategoryEntity {
|
||||
return convertFromSelect(
|
||||
schema,
|
||||
schemaConfig,
|
||||
'categories',
|
||||
category,
|
||||
) as CategoryEntity;
|
||||
},
|
||||
toDb(
|
||||
category: CategoryEntity,
|
||||
{ update }: { update?: boolean } = {},
|
||||
): DbCategory {
|
||||
return (
|
||||
update
|
||||
? convertForUpdate(schema, schemaConfig, 'categories', category)
|
||||
: convertForInsert(schema, schemaConfig, 'categories', category)
|
||||
) as DbCategory;
|
||||
},
|
||||
};
|
||||
|
||||
export const categoryGroupModel = {
|
||||
@@ -97,6 +150,54 @@ export const categoryGroupModel = {
|
||||
const { sort_order, ...rest } = categoryGroup;
|
||||
return { ...rest, hidden: rest.hidden ? 1 : 0 } as DbCategoryGroup;
|
||||
},
|
||||
fromDbArray(
|
||||
grouped: [DbCategoryGroup, ...DbCategory[]][],
|
||||
): CategoryGroupEntity[] {
|
||||
return grouped.map(([group, ...categories]) =>
|
||||
categoryGroupModel.fromDb(group, ...categories),
|
||||
);
|
||||
},
|
||||
fromDb(
|
||||
categoryGroup: DbCategoryGroup,
|
||||
...categories: DbCategory[]
|
||||
): CategoryGroupEntity {
|
||||
const group = convertFromSelect(
|
||||
schema,
|
||||
schemaConfig,
|
||||
'category_groups',
|
||||
categoryGroup,
|
||||
) as CategoryGroupEntity;
|
||||
return {
|
||||
...group,
|
||||
categories: categories
|
||||
.filter(category => category.cat_group === categoryGroup.id)
|
||||
.map(category => categoryModel.fromDb(category)),
|
||||
};
|
||||
},
|
||||
toDb(
|
||||
categoryGroup: CategoryGroupEntity,
|
||||
{ update }: { update?: boolean } = {},
|
||||
): [DbCategoryGroup, ...DbCategory[]] {
|
||||
const group = (
|
||||
update
|
||||
? convertForUpdate(
|
||||
schema,
|
||||
schemaConfig,
|
||||
'category_groups',
|
||||
categoryGroup,
|
||||
)
|
||||
: convertForInsert(
|
||||
schema,
|
||||
schemaConfig,
|
||||
'category_groups',
|
||||
categoryGroup,
|
||||
)
|
||||
) as DbCategoryGroup;
|
||||
const categories =
|
||||
categoryGroup.categories?.map(category => categoryModel.toDb(category)) ||
|
||||
[];
|
||||
return [group, ...categories];
|
||||
},
|
||||
};
|
||||
|
||||
export const payeeModel = {
|
||||
@@ -119,4 +220,22 @@ export const payeeModel = {
|
||||
payee,
|
||||
) as PayeeEntity;
|
||||
},
|
||||
fromDbArray(payees: DbPayee[]): PayeeEntity[] {
|
||||
return payees.map(payee => payeeModel.fromDb(payee));
|
||||
},
|
||||
};
|
||||
|
||||
export const transactionModel = {
|
||||
validate(
|
||||
transaction: Partial<DbTransaction>,
|
||||
{ update }: { update?: boolean } = {},
|
||||
) {
|
||||
requiredFields(
|
||||
'transaction',
|
||||
transaction,
|
||||
['date', 'amount', 'acct'],
|
||||
update,
|
||||
);
|
||||
return transaction;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,7 +5,12 @@ import { captureBreadcrumb } from '../platform/exceptions';
|
||||
import * as sqlite from '../platform/server/sqlite';
|
||||
import { sheetForMonth } from '../shared/months';
|
||||
|
||||
import { DbPreference } from './db';
|
||||
import {
|
||||
DbPreference,
|
||||
DbReflectBudget,
|
||||
DbZeroBudget,
|
||||
DbZeroBudgetMonth,
|
||||
} from './db';
|
||||
import * as Platform from './platform';
|
||||
import { Spreadsheet } from './spreadsheet/spreadsheet';
|
||||
import { resolveName } from './spreadsheet/util';
|
||||
@@ -205,7 +210,7 @@ export async function loadUserBudgets(
|
||||
)) ?? {};
|
||||
|
||||
const table = budgetType === 'report' ? 'reflect_budgets' : 'zero_budgets';
|
||||
const budgets = await db.all(`
|
||||
const budgets = await db.all<DbReflectBudget | DbZeroBudget>(`
|
||||
SELECT * FROM ${table} b
|
||||
LEFT JOIN categories c ON c.id = b.category
|
||||
WHERE c.tombstone = 0
|
||||
@@ -229,7 +234,9 @@ export async function loadUserBudgets(
|
||||
|
||||
// For zero-based budgets, load the buffered amounts
|
||||
if (budgetType !== 'report') {
|
||||
const budgetMonths = await db.all('SELECT * FROM zero_budget_months');
|
||||
const budgetMonths = await db.all<DbZeroBudgetMonth>(
|
||||
'SELECT * FROM zero_budget_months',
|
||||
);
|
||||
for (const budgetMonth of budgetMonths) {
|
||||
const sheetName = sheetForMonth(budgetMonth.id);
|
||||
sheet.set(`${sheetName}!buffered`, budgetMonth.buffered);
|
||||
|
||||
@@ -744,11 +744,13 @@ async function _fullSync(
|
||||
|
||||
if (rebuiltMerkle.trie.hash === res.merkle.hash) {
|
||||
// Rebuilding the merkle worked... but why?
|
||||
const clocks = await db.all('SELECT * FROM messages_clock');
|
||||
const clocks = await db.all<db.DbClockMessage>(
|
||||
'SELECT * FROM messages_clock',
|
||||
);
|
||||
if (clocks.length !== 1) {
|
||||
console.log('Bad number of clocks:', clocks.length);
|
||||
}
|
||||
const hash = deserializeClock(clocks[0]).merkle.hash;
|
||||
const hash = deserializeClock(clocks[0].clock).merkle.hash;
|
||||
console.log('Merkle hash in db:', hash);
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,10 @@ describe('sync migrations', () => {
|
||||
await sendMessages(msgs);
|
||||
await tracer.expect('applied');
|
||||
|
||||
const transactions = await db.all('SELECT * FROM transactions', []);
|
||||
const transactions = await db.all<db.DbTransaction>(
|
||||
'SELECT * FROM transactions',
|
||||
[],
|
||||
);
|
||||
for (const trans of transactions) {
|
||||
const transMsgs = msgs
|
||||
.filter(msg => msg.row === trans.id)
|
||||
|
||||
@@ -54,8 +54,12 @@ describe('Sync', () => {
|
||||
expect(getClock().timestamp.toString()).toEqual(timestamp.toString());
|
||||
expect(mockSyncServer.getClock().merkle).toEqual(getClock().merkle);
|
||||
|
||||
expect(await db.all('SELECT * FROM messages_crdt')).toMatchSnapshot();
|
||||
expect(await db.all('SELECT * FROM messages_clock')).toMatchSnapshot();
|
||||
expect(
|
||||
await db.all<db.DbCrdtMessage>('SELECT * FROM messages_crdt'),
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
await db.all<db.DbClockMessage>('SELECT * FROM messages_clock'),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should resend old messages to the server', async () => {
|
||||
|
||||
@@ -13,7 +13,11 @@ export const app = createApp<ToolsHandlers>();
|
||||
app.method('tools/fix-split-transactions', async () => {
|
||||
// 1. Check for child transactions that have a blank payee, and set
|
||||
// the payee to whatever the parent has
|
||||
const blankPayeeRows = await db.all(`
|
||||
const blankPayeeRows = await db.all<
|
||||
db.DbViewTransactionInternal & {
|
||||
parentPayee: db.DbViewTransactionInternal['payee'];
|
||||
}
|
||||
>(`
|
||||
SELECT t.*, p.payee AS parentPayee FROM v_transactions_internal t
|
||||
LEFT JOIN v_transactions_internal p ON t.parent_id = p.id
|
||||
WHERE t.is_child = 1 AND t.payee IS NULL AND p.payee IS NOT NULL
|
||||
@@ -29,7 +33,9 @@ app.method('tools/fix-split-transactions', async () => {
|
||||
|
||||
// 2. Make sure the "cleared" flag is synced up with the parent
|
||||
// transactions
|
||||
const clearedRows = await db.all(`
|
||||
const clearedRows = await db.all<
|
||||
Pick<db.DbViewTransactionInternal, 'id' | 'cleared'>
|
||||
>(`
|
||||
SELECT t.id, p.cleared FROM v_transactions_internal t
|
||||
LEFT JOIN v_transactions_internal p ON t.parent_id = p.id
|
||||
WHERE t.is_child = 1 AND t.cleared != p.cleared
|
||||
@@ -45,7 +51,7 @@ app.method('tools/fix-split-transactions', async () => {
|
||||
|
||||
// 3. Mark the `tombstone` field as true on any child transactions
|
||||
// that have a dead parent
|
||||
const deletedRows = await db.all(`
|
||||
const deletedRows = await db.all<db.DbViewTransactionInternal>(`
|
||||
SELECT t.* FROM v_transactions_internal t
|
||||
LEFT JOIN v_transactions_internal p ON t.parent_id = p.id
|
||||
WHERE t.is_child = 1 AND t.tombstone = 0 AND (p.tombstone = 1 OR p.id IS NULL)
|
||||
@@ -74,7 +80,9 @@ app.method('tools/fix-split-transactions', async () => {
|
||||
});
|
||||
|
||||
// 5. Fix transfers that should not have categories
|
||||
const brokenTransfers = await db.all(`
|
||||
const brokenTransfers = await db.all<
|
||||
Pick<db.DbViewTransactionInternal, 'id'>
|
||||
>(`
|
||||
SELECT t1.id
|
||||
FROM v_transactions_internal t1
|
||||
JOIN accounts a1 ON t1.account = a1.id
|
||||
|
||||
@@ -12,7 +12,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);
|
||||
@@ -57,7 +57,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -175,7 +175,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
|
||||
`);
|
||||
@@ -800,7 +800,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
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as transfer from './transfer';
|
||||
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
|
||||
|
||||
@@ -8,4 +8,5 @@ export interface CategoryEntity {
|
||||
sort_order?: number;
|
||||
tombstone?: boolean;
|
||||
hidden?: boolean;
|
||||
goal_def?: string;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/4250.md
Normal file
6
upcoming-release-notes/4250.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[TypeScript] Make `db.all` generic to make it easy to type DB query results.
|
||||
Reference in New Issue
Block a user