diff --git a/packages/api/methods.ts b/packages/api/methods.ts index e0e3069f95..d3f98c3a24 100644 --- a/packages/api/methods.ts +++ b/packages/api/methods.ts @@ -52,10 +52,18 @@ export async function batchBudgetUpdates(func) { } } +/** + * @deprecated Please use `aqlQuery` instead. + * This function will be removed in a future release. + */ export function runQuery(query) { return send('api/query', { query: query.serialize() }); } +export function aqlQuery(query) { + return send('api/query', { query: query.serialize() }); +} + export function getBudgetMonths() { return send('api/budget-months'); } diff --git a/packages/desktop-client/src/components/accounts/Account.tsx b/packages/desktop-client/src/components/accounts/Account.tsx index 36d8eb1313..5df7951444 100644 --- a/packages/desktop-client/src/components/accounts/Account.tsx +++ b/packages/desktop-client/src/components/accounts/Account.tsx @@ -38,7 +38,7 @@ import { updateNewTransactions, } from 'loot-core/client/queries/queriesSlice'; import { - runQuery, + aqlQuery, pagedQuery, type PagedQuery, } from 'loot-core/client/query-helpers'; @@ -442,7 +442,7 @@ class AccountInternal extends PureComponent< return []; } - const { data } = await runQuery(this.paged.query.select('id')); + const { data } = await aqlQuery(this.paged.query.select('id')); // Remember, this is the `grouped` split type so we need to deal // with the `subtransactions` property return data.reduce((arr: string[], t: TransactionEntity) => { @@ -679,7 +679,7 @@ class AccountInternal extends PureComponent< return null; } - const { data } = await runQuery( + const { data } = await aqlQuery( this.paged.query .options({ splits: 'none' }) .select([{ balance: { $sumOver: '$amount' } }]), @@ -909,7 +909,7 @@ class AccountInternal extends PureComponent< return 0; } - const { data: amount } = await runQuery( + const { data: amount } = await aqlQuery( this.paged.query.calculate({ $sum: '$amount' }), ); return amount; @@ -936,7 +936,7 @@ class AccountInternal extends PureComponent< const { accountId } = this.props; - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ cleared: true, reconciled: false, account: accountId }) .select('*') @@ -984,7 +984,7 @@ class AccountInternal extends PureComponent< const { reconcileAmount } = this.state; - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ cleared: true, account: accountId }) .select('*') @@ -1084,7 +1084,7 @@ class AccountInternal extends PureComponent< onMakeAsSplitTransaction = async (ids: string[]) => { this.setState({ workingHard: true }); - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*') @@ -1123,7 +1123,7 @@ class AccountInternal extends PureComponent< onMakeAsNonSplitTransactions = async (ids: string[]) => { this.setState({ workingHard: true }); - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*') @@ -1213,7 +1213,7 @@ class AccountInternal extends PureComponent< confirmReason: string, onConfirm: (ids: string[]) => void, ) => { - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids }, reconciled: true }) .select('*') @@ -1255,7 +1255,7 @@ class AccountInternal extends PureComponent< }; onCreateRule = async (ids: string[]) => { - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*') diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index 2b4ce69b4c..f9da1821c5 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -33,7 +33,7 @@ import { import { pushModal } from 'loot-core/client/modals/modalsSlice'; import * as Platform from 'loot-core/client/platform'; import { setLastTransaction } from 'loot-core/client/queries/queriesSlice'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; import { q } from 'loot-core/shared/query'; @@ -1107,7 +1107,7 @@ function TransactionEditUnconnected({ // The edit item components expect to work with a flat array of // transactions when handling splits, so we call ungroupTransactions to // flatten parent and children into one array. - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: transactionId }) .select('*') diff --git a/packages/desktop-client/src/components/modals/EditRuleModal.jsx b/packages/desktop-client/src/components/modals/EditRuleModal.jsx index e8652f4c00..c812356dff 100644 --- a/packages/desktop-client/src/components/modals/EditRuleModal.jsx +++ b/packages/desktop-client/src/components/modals/EditRuleModal.jsx @@ -25,7 +25,7 @@ import { v4 as uuid } from 'uuid'; import { useSchedules } from 'loot-core/client/data-hooks/schedules'; import { initiallyLoadPayees } from 'loot-core/client/queries/queriesSlice'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { enableUndo, disableUndo } from 'loot-core/client/undo'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -840,7 +840,7 @@ export function EditRuleModal({ const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and'; const parentOnlyCondition = actionSplits.length > 1 ? { is_child: false } : {}; - const { data: transactions } = await runQuery( + const { data: transactions } = await aqlQuery( q('transactions') .filter({ [conditionsOpKey]: filters, ...parentOnlyCondition }) .select('*'), diff --git a/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts index 2e61de041f..b4e0409229 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts @@ -1,6 +1,6 @@ import * as d from 'date-fns'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { type useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -90,7 +90,7 @@ export function calendarSpreadsheet( let expenseData; try { - expenseData = await runQuery( + expenseData = await aqlQuery( makeRootQuery().filter({ $and: { amount: { $lt: 0 } }, }), @@ -102,7 +102,7 @@ export function calendarSpreadsheet( let incomeData; try { - incomeData = await runQuery( + incomeData = await aqlQuery( makeRootQuery().filter({ $and: { amount: { $gt: 0 } }, }), diff --git a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts index 372fa697ac..ed8d55994a 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts @@ -1,6 +1,6 @@ import * as d from 'date-fns'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { type useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -98,7 +98,7 @@ export function createCustomSpreadsheet({ let assets: QueryDataEntity[]; let debts: QueryDataEntity[]; [assets, debts] = await Promise.all([ - runQuery( + aqlQuery( makeQuery( 'assets', startDate, @@ -108,7 +108,7 @@ export function createCustomSpreadsheet({ filters, ), ).then(({ data }) => data), - runQuery( + aqlQuery( makeQuery( 'debts', startDate, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts index 225cb8831c..42d7205b9a 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts @@ -1,4 +1,4 @@ -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { type useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -49,7 +49,7 @@ export function createGroupedSpreadsheet({ let assets: QueryDataEntity[]; let debts: QueryDataEntity[]; [assets, debts] = await Promise.all([ - runQuery( + aqlQuery( makeQuery( 'assets', startDate, @@ -59,7 +59,7 @@ export function createGroupedSpreadsheet({ filters, ), ).then(({ data }) => data), - runQuery( + aqlQuery( makeQuery( 'debts', startDate, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/net-worth-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/net-worth-spreadsheet.ts index c754640738..59530d0bae 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/net-worth-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/net-worth-spreadsheet.ts @@ -1,7 +1,7 @@ import * as d from 'date-fns'; import keyBy from 'lodash/keyBy'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { type useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -41,7 +41,7 @@ export function createSpreadsheet( const data = await Promise.all( accounts.map(async acct => { const [starting, balances]: [number, Balance[]] = await Promise.all([ - runQuery( + aqlQuery( q('transactions') .filter({ [conditionsOpKey]: filters, @@ -51,7 +51,7 @@ export function createSpreadsheet( .calculate({ $sum: '$amount' }), ).then(({ data }) => data), - runQuery( + aqlQuery( q('transactions') .filter({ [conditionsOpKey]: filters, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts index 1b5e975b74..06cb3849be 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts @@ -1,7 +1,7 @@ // @ts-strict-ignore import keyBy from 'lodash/keyBy'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { type useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -59,7 +59,7 @@ export function createSpendingSpreadsheet({ const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and'; const [assets, debts] = await Promise.all([ - runQuery( + aqlQuery( makeQuery( 'assets', startDate, @@ -69,7 +69,7 @@ export function createSpendingSpreadsheet({ filters, ), ).then(({ data }) => data), - runQuery( + aqlQuery( makeQuery( 'debts', startDate, @@ -82,7 +82,7 @@ export function createSpendingSpreadsheet({ ]); const [assetsTo, debtsTo] = await Promise.all([ - runQuery( + aqlQuery( makeQuery( 'assets', startDateTo, @@ -92,7 +92,7 @@ export function createSpendingSpreadsheet({ filters, ), ).then(({ data }) => data), - runQuery( + aqlQuery( makeQuery( 'debts', startDateTo, @@ -114,7 +114,7 @@ export function createSpendingSpreadsheet({ const budgetMonth = parseInt(compare.replace('-', '')); const [budgets] = await Promise.all([ - runQuery( + aqlQuery( q('zero_budgets') .filter({ $and: [{ month: { $eq: budgetMonth } }], diff --git a/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts index 07fb5d4168..ebd7212c33 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts @@ -1,6 +1,6 @@ import * as d from 'date-fns'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { type useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -115,7 +115,7 @@ export function summarySpreadsheet( let data; try { - data = await runQuery(query); + data = await aqlQuery(query); } catch (error) { console.error('Error executing query:', error); return; @@ -278,7 +278,7 @@ async function calculatePercentage( let divisorData; try { - divisorData = (await runQuery(query)) as { data: { amount: number }[] }; + divisorData = (await aqlQuery(query)) as { data: { amount: number }[] }; } catch (error) { console.error('Error executing divisor query:', error); return { diff --git a/packages/desktop-client/src/components/reports/util.ts b/packages/desktop-client/src/components/reports/util.ts index 52203b57b1..d7c4c45c1a 100644 --- a/packages/desktop-client/src/components/reports/util.ts +++ b/packages/desktop-client/src/components/reports/util.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import type { Query } from 'loot-core/shared/query'; export function fromDateRepr(date: string): string { @@ -12,7 +12,7 @@ export async function runAll( ): Promise { const data = await Promise.all( queries.map(q => { - return runQuery(q).then(({ data }) => data); + return aqlQuery(q).then(({ data }) => data); }), ); cb(data); diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx index de6ea128be..145f6bac60 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx @@ -8,7 +8,7 @@ import { Stack } from '@actual-app/components/stack'; import { theme } from '@actual-app/components/theme'; import { View } from '@actual-app/components/view'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { send } from 'loot-core/platform/client/fetch'; import { q } from 'loot-core/shared/query'; import { getRecurringDescription } from 'loot-core/shared/schedules'; @@ -177,7 +177,7 @@ export function DiscoverSchedules() { }); if (filters.length > 0) { - const { data: transactions } = await runQuery( + const { data: transactions } = await aqlQuery( q('transactions').filter({ $and: filters }).select('id'), ); diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.tsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.tsx index 76e35849e4..d7a85e73ed 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleDetails.tsx +++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.tsx @@ -15,7 +15,7 @@ import { pushModal, } from 'loot-core/client/modals/modalsSlice'; import { getPayeesById } from 'loot-core/client/queries/queriesSlice'; -import { runQuery, liveQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery, liveQuery } from 'loot-core/client/query-helpers'; import { send, sendCatch } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; import { q } from 'loot-core/shared/query'; @@ -316,7 +316,7 @@ export function ScheduleDetails({ id, transaction }: ScheduleDetailsProps) { ); async function loadSchedule() { - const { data } = await runQuery(q('schedules').filter({ id }).select('*')); + const { data } = await aqlQuery(q('schedules').filter({ id }).select('*')); return data[0]; } @@ -480,7 +480,7 @@ export function ScheduleDetails({ id, transaction }: ScheduleDetailsProps) { async function onSave(close: () => void, schedule: Partial) { dispatch({ type: 'form-error', error: null }); if (state.fields.name) { - const { data: sameName } = await runQuery( + const { data: sameName } = await aqlQuery( q('schedules').filter({ name: state.fields.name }).select('id'), ); if (sameName.length > 0 && sameName[0].id !== schedule.id) { diff --git a/packages/desktop-client/src/hooks/useTransactionBatchActions.ts b/packages/desktop-client/src/hooks/useTransactionBatchActions.ts index 22792d565c..d1b8aa2fcd 100644 --- a/packages/desktop-client/src/hooks/useTransactionBatchActions.ts +++ b/packages/desktop-client/src/hooks/useTransactionBatchActions.ts @@ -1,5 +1,5 @@ import { pushModal } from 'loot-core/client/modals/modalsSlice'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { validForTransfer } from 'loot-core/client/transfer'; import { send } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; @@ -60,7 +60,7 @@ export function useTransactionBatchActions() { const dispatch = useDispatch(); const onBatchEdit = async ({ name, ids, onSuccess }: BatchEditProps) => { - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*') @@ -267,7 +267,7 @@ export function useTransactionBatchActions() { const onBatchDuplicate = async ({ ids, onSuccess }: BatchDuplicateProps) => { const onConfirmDuplicate = async (ids: Array) => { - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*') @@ -316,7 +316,7 @@ export function useTransactionBatchActions() { ? `Are you sure you want to delete these ${ids.length} transaction${ids.length > 1 ? 's' : ''}?` : undefined, onConfirm: async () => { - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*') @@ -386,7 +386,7 @@ export function useTransactionBatchActions() { account, onSuccess, }: BatchLinkScheduleProps) => { - const { data: transactions } = await runQuery( + const { data: transactions } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*') @@ -429,7 +429,7 @@ export function useTransactionBatchActions() { confirmReason: string, onConfirm: (ids: Array) => void, ) => { - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids }, reconciled: true }) .select('*') @@ -461,7 +461,7 @@ export function useTransactionBatchActions() { onSuccess: (ids: string[]) => void, ) => { const onConfirmTransfer = async (ids: string[]) => { - const { data: transactions } = await runQuery( + const { data: transactions } = await aqlQuery( q('transactions') .filter({ id: { $oneof: ids } }) .select('*'), diff --git a/packages/desktop-client/src/index.tsx b/packages/desktop-client/src/index.tsx index c78e4b9fbe..c376c13d88 100644 --- a/packages/desktop-client/src/index.tsx +++ b/packages/desktop-client/src/index.tsx @@ -19,7 +19,7 @@ import * as modalsSlice from 'loot-core/client/modals/modalsSlice'; import * as notificationsSlice from 'loot-core/client/notifications/notificationsSlice'; import * as prefsSlice from 'loot-core/client/prefs/prefsSlice'; import * as queriesSlice from 'loot-core/client/queries/queriesSlice'; -import { runQuery } from 'loot-core/client/query-helpers'; +import { aqlQuery } from 'loot-core/client/query-helpers'; import { store } from 'loot-core/client/store'; import { redo, undo } from 'loot-core/client/undo'; import * as usersSlice from 'loot-core/client/users/usersSlice'; @@ -80,7 +80,7 @@ window.__actionsForMenu = { // Expose send for fun! window.$send = send; -window.$query = runQuery; +window.$query = aqlQuery; window.$q = q; const container = document.getElementById('root'); @@ -107,7 +107,7 @@ declare global { }; $send: typeof send; - $query: typeof runQuery; + $query: typeof aqlQuery; $q: typeof q; } } diff --git a/packages/loot-core/src/client/query-helpers.test.ts b/packages/loot-core/src/client/query-helpers.test.ts index 4af5dc231f..fb466a1d34 100644 --- a/packages/loot-core/src/client/query-helpers.test.ts +++ b/packages/loot-core/src/client/query-helpers.test.ts @@ -4,7 +4,7 @@ import { subDays } from '../shared/months'; import { q } from '../shared/query'; import { tracer } from '../shared/test-helpers'; -import { runQuery, liveQuery, pagedQuery } from './query-helpers'; +import { aqlQuery, liveQuery, pagedQuery } from './query-helpers'; function wait(n) { return new Promise(resolve => setTimeout(() => resolve(`wait(${n})`), n)); @@ -121,13 +121,13 @@ function initPagingServer( } describe('query helpers', () => { - it('runQuery runs a query', async () => { + it('aqlQuery runs a query', async () => { initServer({ query: query => Promise.resolve({ data: query, dependencies: [] }), }); const query = q('transactions').select('*'); - const { data } = await runQuery(query); + const { data } = await aqlQuery(query); expect(data).toEqual(query.serialize()); }); diff --git a/packages/loot-core/src/client/query-helpers.ts b/packages/loot-core/src/client/query-helpers.ts index 1f5ebed926..28839f3df9 100644 --- a/packages/loot-core/src/client/query-helpers.ts +++ b/packages/loot-core/src/client/query-helpers.ts @@ -3,7 +3,7 @@ import { listen, send } from '../platform/client/fetch'; import { once } from '../shared/async'; import { getPrimaryOrderBy, type Query } from '../shared/query'; -export async function runQuery(query: Query) { +export async function aqlQuery(query: Query) { return send('query', query.serialize()); } @@ -146,7 +146,7 @@ export class LiveQuery { run = () => { this.subscribe(); - return this.fetchData(() => runQuery(this._query)); + return this.fetchData(() => aqlQuery(this._query)); }; static runLiveQuery = ( @@ -205,7 +205,7 @@ export class LiveQuery { }; protected fetchData = async ( - runQuery: () => Promise<{ + aqlQuery: () => Promise<{ data: Data; dependencies: string[]; }>, @@ -218,7 +218,7 @@ export class LiveQuery { this.inflightRequestId = reqId; try { - const { data, dependencies } = await runQuery(); + const { data, dependencies } = await aqlQuery(); // Regardless if this request was cancelled or not, save the // dependencies. The query can't change so all requests will @@ -278,7 +278,7 @@ export class PagedQuery extends LiveQuery { } private fetchCount = () => { - return runQuery(this.query.calculate({ $count: '*' })).then(({ data }) => { + return aqlQuery(this.query.calculate({ $count: '*' })).then(({ data }) => { this._totalCount = data; }); }; @@ -294,7 +294,7 @@ export class PagedQuery extends LiveQuery { // If data is null, we haven't fetched anything yet so just // fetch the first page - return runQuery( + return aqlQuery( this.query.limit( this.data == null ? this._pageCount @@ -338,7 +338,7 @@ export class PagedQuery extends LiveQuery { const { field, order } = orderDesc; - let result = await runQuery(this.query.filter({ id }).select(field)); + let result = await aqlQuery(this.query.filter({ id }).select(field)); if (result.data.length === 0) { // This row is not part of this set anymore, we can't do // this. We stop early to avoid possibly pulling in a ton of @@ -347,7 +347,7 @@ export class PagedQuery extends LiveQuery { } const fullRow = result.data[0]; - result = await runQuery( + result = await aqlQuery( this.query.filter({ [field]: { [order === 'asc' ? '$lte' : '$gte']: fullRow[field], @@ -358,7 +358,7 @@ export class PagedQuery extends LiveQuery { // Load in an extra page to make room for the UI to show some // data after it - result = await runQuery( + result = await aqlQuery( this.query .filter({ [field]: { @@ -391,7 +391,7 @@ export class PagedQuery extends LiveQuery { const previousData = this.data; if (!this._hasReachedEnd) { - const { data } = await runQuery( + const { data } = await aqlQuery( this.query.limit(this._pageCount).offset(previousData.length), ); diff --git a/packages/loot-core/src/mocks/budget.ts b/packages/loot-core/src/mocks/budget.ts index 67b8fef2fa..a619158c93 100644 --- a/packages/loot-core/src/mocks/budget.ts +++ b/packages/loot-core/src/mocks/budget.ts @@ -2,7 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { addTransactions } from '../server/accounts/sync'; -import { runQuery as aqlQuery } from '../server/aql'; +import { aqlQuery } from '../server/aql'; import * as budgetActions from '../server/budget/actions'; import * as budget from '../server/budget/base'; import * as db from '../server/db'; diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts index 08c3683a40..610db049ec 100644 --- a/packages/loot-core/src/server/accounts/sync.ts +++ b/packages/loot-core/src/server/accounts/sync.ts @@ -20,7 +20,7 @@ import { SimpleFinBatchSyncResponse, TransactionEntity, } from '../../types/models'; -import { runQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import { runMutator } from '../mutators'; import { post } from '../post'; @@ -75,7 +75,7 @@ async function updateAccountBalance(id, balance) { async function getAccountOldestTransaction(id): Promise { return ( - await runQuery( + await aqlQuery( q('transactions') .filter({ account: id, @@ -373,17 +373,17 @@ async function normalizeBankSyncTransactions(transactions, acctId) { const payeesToCreate = new Map(); const [customMappingsRaw, importPending, importNotes] = await Promise.all([ - runQuery( + aqlQuery( q('preferences') .filter({ id: `custom-sync-mappings-${acctId}` }) .select('value'), ).then(data => data?.data?.[0]?.value), - runQuery( + aqlQuery( q('preferences') .filter({ id: `sync-import-pending-${acctId}` }) .select('value'), ).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true'), - runQuery( + aqlQuery( q('preferences') .filter({ id: `sync-import-notes-${acctId}` }) .select('value'), @@ -618,7 +618,7 @@ export async function matchTransactions( ) { console.log('Performing transaction reconciliation matching'); - const reimportDeleted = await runQuery( + const reimportDeleted = await aqlQuery( q('preferences') .filter({ id: `sync-reimport-deleted-${acctId}` }) .select('value'), diff --git a/packages/loot-core/src/server/api.ts b/packages/loot-core/src/server/api.ts index df8daff177..a697ff1058 100644 --- a/packages/loot-core/src/server/api.ts +++ b/packages/loot-core/src/server/api.ts @@ -29,7 +29,7 @@ import { payeeModel, remoteFileModel, } from './api-models'; -import { runQuery as aqlQuery } from './aql'; +import { aqlQuery } from './aql'; import * as cloudStorage from './cloud-storage'; import { type RemoteFile } from './cloud-storage'; import * as db from './db'; diff --git a/packages/loot-core/src/server/aql/compiler.ts b/packages/loot-core/src/server/aql/compiler.ts index 02ae4ffdd1..d35477b47f 100644 --- a/packages/loot-core/src/server/aql/compiler.ts +++ b/packages/loot-core/src/server/aql/compiler.ts @@ -1001,12 +1001,15 @@ export function isAggregateQuery(queryState) { }); } +// TODO: Type this based on schema/index +type Schema = unknown; + export type SchemaConfig = { tableViews?: - | Record - | ((name: string, config: { withDead; isJoin; tableOptions }) => unknown); + | Record + | ((name: string, config: { withDead; isJoin; tableOptions }) => string); tableFilters?: (name: string) => unknown[]; - customizeQuery?: (queryString: QueryState) => QueryState; + customizeQuery?: (queryState: QueryState) => QueryState; views?: Record< string, { @@ -1015,9 +1018,46 @@ export type SchemaConfig = { } >; }; + +// Types per field. Should be based on the schema. +export type OutputTypes = Map; + +type NamedParameter = { + type: string; + paramName: string; + paramType?: string; + value: string; +}; + +// TODO: Type this +type CompileStack = unknown[]; + +export type CompilerState = { + schema: Schema; + implicitTableName: string; + implicitTableId: string; + paths: Map; + dependencies: string[]; + compileStack: CompileStack; + outputTypes: OutputTypes; + validateRefs: boolean; + namedParameters: NamedParameter[]; +}; + +export type SqlPieces = { + select: string; + from: string; + joins: string; + where: string; + groupBy: string; + orderBy: string; + limit: number | null; + offset: number | null; +}; + export function compileQuery( - queryState, - schema, + queryState: QueryState, + schema: Schema, schemaConfig: SchemaConfig = {}, ) { const { withDead, validateRefs = true, tableOptions, rawMode } = queryState; @@ -1070,7 +1110,7 @@ export function compileQuery( let joins = ''; let groupBy = ''; let orderBy = ''; - const state = { + const state: CompilerState = { schema, implicitTableName: tableName, implicitTableId: tableRef(tableName), @@ -1128,7 +1168,7 @@ export function compileQuery( throw e; } - const sqlPieces = { + const sqlPieces: SqlPieces = { select, from: tableRef(tableName), joins, @@ -1145,15 +1185,19 @@ export function compileQuery( }; } -export function defaultConstructQuery(queryState, state, sqlPieces) { +export function defaultConstructQuery( + queryState: QueryState, + compilerState: CompilerState, + sqlPieces: SqlPieces, +) { const s = sqlPieces; const where = queryState.withDead ? s.where : addTombstone( - state.schema, - state.implicitTableName, - state.implicitTableId, + compilerState.schema, + compilerState.implicitTableName, + compilerState.implicitTableId, s.where, ); @@ -1169,9 +1213,9 @@ export function defaultConstructQuery(queryState, state, sqlPieces) { } export function generateSQLWithState( - queryState, - schema?: unknown, - schemaConfig?: unknown, + queryState: QueryState, + schema?: Schema, + schemaConfig?: SchemaConfig, ) { const { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig); return { sql: defaultConstructQuery(queryState, state, sqlPieces), state }; diff --git a/packages/loot-core/src/server/aql/exec.test.ts b/packages/loot-core/src/server/aql/exec.test.ts index c16f3af1aa..fd1a285cc7 100644 --- a/packages/loot-core/src/server/aql/exec.test.ts +++ b/packages/loot-core/src/server/aql/exec.test.ts @@ -18,8 +18,8 @@ function repeat(arr, times) { return result; } -function runQuery(query, options?: unknown) { - return aql.runQuery(schema, schemaConfig, query, options); +function compileAndRunAqlQuery(query, options?: unknown) { + return aql.compileAndRunAqlQuery(schema, schemaConfig, query, options); } async function insertTransactions(repeatTimes = 1) { @@ -71,17 +71,19 @@ async function insertTransactions(repeatTimes = 1) { } } -describe('runQuery', () => { +describe('compileAndRunQuery', () => { it('converts output types', async () => { await insertTransactions(); // date - let { data } = await runQuery(q('transactions').select('date').serialize()); + let { data } = await compileAndRunAqlQuery( + q('transactions').select('date').serialize(), + ); expect(data[0].date).toBe('2020-01-04'); // date-month data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions') .select({ month: { $month: '$date' } }) .serialize(), @@ -91,7 +93,7 @@ describe('runQuery', () => { // date-year data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions') .select({ year: { $year: '$date' } }) .serialize(), @@ -101,7 +103,7 @@ describe('runQuery', () => { // boolean data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions').select(['is_child', 'is_parent']).raw().serialize(), ) ).data; @@ -121,7 +123,7 @@ describe('runQuery', () => { cleared: true, }); - let { data } = await runQuery( + let { data } = await compileAndRunAqlQuery( q('transactions') .filter({ amount: { $lt: { $neg: ':amount' } } }) .select() @@ -131,7 +133,7 @@ describe('runQuery', () => { expect(data[0].id).toBe(transId); data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions') .filter({ date: { $transform: '$month', $eq: { $month: ':month' } } }) .select('date') @@ -142,7 +144,7 @@ describe('runQuery', () => { expect(data[0].id).toBe(transId); data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions') .filter({ date: { $transform: '$year', $eq: { $year: ':month' } } }) .select('date') @@ -153,7 +155,7 @@ describe('runQuery', () => { expect(data[0].id).toBe(transId); data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions') .filter({ cleared: ':cleared' }) .select('date') @@ -192,14 +194,18 @@ describe('runQuery', () => { .select() .serialize(); - let { data } = await runQuery(queryState, { params: { category: null } }); + let { data } = await compileAndRunAqlQuery(queryState, { + params: { category: null }, + }); expect(data[0].id).toBe(transNoCat); - data = (await runQuery(queryState, { params: { category: 'cat' } })).data; + data = ( + await compileAndRunAqlQuery(queryState, { params: { category: 'cat' } }) + ).data; expect(data[0].id).toBe(transCat); data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions') .filter({ category: { $ne: ':category' } }) .select('category') @@ -218,7 +224,7 @@ describe('runQuery', () => { ); data = ( - await runQuery( + await compileAndRunAqlQuery( q('transactions') .filter({ category: { $ne: ':category' } }) .select('category') @@ -245,7 +251,7 @@ describe('runQuery', () => { amount: -5001, }); - const { data } = await runQuery( + const { data } = await compileAndRunAqlQuery( q('transactions') .filter({ amount: { $lt: { $neg: ':amount' } }, @@ -267,7 +273,7 @@ describe('runQuery', () => { const ids = rows.slice(0, 3).map(row => row.id); ids.sort(); - const { data } = await runQuery( + const { data } = await compileAndRunAqlQuery( q('transactions') .filter({ id: { $oneof: repeat(ids, 1000) }, amount: { $lt: 50 } }) .select('id') diff --git a/packages/loot-core/src/server/aql/exec.ts b/packages/loot-core/src/server/aql/exec.ts index 68e29a0c9e..43b3c47975 100644 --- a/packages/loot-core/src/server/aql/exec.ts +++ b/packages/loot-core/src/server/aql/exec.ts @@ -1,7 +1,15 @@ // @ts-strict-ignore +import { QueryState } from '../../shared/query'; import * as db from '../db'; -import { compileQuery, defaultConstructQuery } from './compiler'; +import { + compileQuery, + CompilerState, + defaultConstructQuery, + OutputTypes, + SchemaConfig, + SqlPieces, +} from './compiler'; import { convertInputType, convertOutputType } from './schema-helpers'; // TODO (compiler): @@ -16,7 +24,7 @@ import { convertInputType, convertOutputType } from './schema-helpers'; // global "field lookup" table that other filter/groupBy/etc // expressions can reference -function applyTypes(data, outputTypes) { +function applyTypes(data: Record[], outputTypes: OutputTypes) { for (let i = 0; i < data.length; i++) { const item = data[i]; Object.keys(item).forEach(name => { @@ -26,25 +34,42 @@ function applyTypes(data, outputTypes) { } export async function execQuery( - queryState, - state, - sqlPieces, - params, - outputTypes, + queryState: QueryState, + compilerState: CompilerState, + sqlPieces: SqlPieces, + params: (string | number)[], + outputTypes: OutputTypes, ) { - const sql = defaultConstructQuery(queryState, state, sqlPieces); - const data = await db.all(sql, params); + const sql = defaultConstructQuery(queryState, compilerState, sqlPieces); + const data = await db.all>(sql, params); applyTypes(data, outputTypes); return data; } -export async function runCompiledQuery( - query, - pieces, - state, - { params = {}, executors = {} } = {}, +export type AqlQueryExecutor = ( + compilerState: CompilerState, + queryState: QueryState, + sqlPieces: SqlPieces, + params: (string | number)[], + outputTypes: OutputTypes, +) => Promise[]>; + +type AqlQueryParamName = string; +type AqlQueryParamValue = unknown; +export type AqlQueryParams = Record; + +export type RunCompiledAqlQueryOptions = { + params?: AqlQueryParams; + executors?: Record; +}; + +export async function runCompiledAqlQuery( + queryState: QueryState, + sqlPieces: SqlPieces, + compilerState: CompilerState, + { params = {}, executors = {} }: RunCompiledAqlQueryOptions = {}, ) { - const paramArray = state.namedParameters.map(param => { + const paramArray = compilerState.namedParameters.map(param => { const name = param.paramName; if (params[name] === undefined) { throw new Error(`Parameter ${name} not provided to query`); @@ -52,35 +77,53 @@ export async function runCompiledQuery( return convertInputType(params[name], param.paramType); }); - let data; - if (executors[state.implicitTableName]) { - data = await executors[state.implicitTableName]( - state, - query, - pieces, + let data: Record[] = []; + if (executors[compilerState.implicitTableName]) { + data = await executors[compilerState.implicitTableName]( + compilerState, + queryState, + sqlPieces, paramArray, - state.outputTypes, + compilerState.outputTypes, ); } else { - data = await execQuery(query, state, pieces, paramArray, state.outputTypes); + data = await execQuery( + queryState, + compilerState, + sqlPieces, + paramArray, + compilerState.outputTypes, + ); } - if (query.calculation) { + if (queryState.calculation) { if (data.length > 0) { const row = data[0]; const k = Object.keys(row)[0]; // TODO: the function being run should be the one to // determine the default value, not hardcoded as 0 - data = row[k] || 0; + return row[k] || 0; } else { - data = null; + return null; } } + return data; } -export async function runQuery(schema, schemaConfig, query, options) { - const { sqlPieces, state } = compileQuery(query, schema, schemaConfig); - const data = await runCompiledQuery(query, sqlPieces, state, options); +export async function compileAndRunAqlQuery( + schema, + schemaConfig: SchemaConfig, + queryState: QueryState, + options: RunCompiledAqlQueryOptions, +) { + const { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data: any = await runCompiledAqlQuery( + queryState, + sqlPieces, + state, + options, + ); return { data, dependencies: state.dependencies }; } diff --git a/packages/loot-core/src/server/aql/index.ts b/packages/loot-core/src/server/aql/index.ts index 764408a608..47af3d926b 100644 --- a/packages/loot-core/src/server/aql/index.ts +++ b/packages/loot-core/src/server/aql/index.ts @@ -1,3 +1,14 @@ +import { Query, type QueryState } from '../../shared/query'; + +import { CompilerState, SqlPieces } from './compiler'; +import { + AqlQueryParams, + compileAndRunAqlQuery, + runCompiledAqlQuery, +} from './exec'; +import { schema, schemaConfig } from './schema'; +import { schemaExecutors } from './schema/executors'; + export { convertForInsert, convertForUpdate, @@ -7,4 +18,26 @@ export { export { compileQuery } from './compiler'; export { makeViews } from './views'; export { schema, schemaConfig } from './schema'; -export { runQuery, runCompiledQuery } from './schema/run-query'; + +export function aqlCompiledQuery( + queryState: QueryState, + sqlPieces: SqlPieces, + compilerState: CompilerState, + params?: AqlQueryParams, +) { + return runCompiledAqlQuery(queryState, sqlPieces, compilerState, { + params, + executors: schemaExecutors, + }); +} + +export function aqlQuery(query: Query | QueryState, params?: AqlQueryParams) { + if (query instanceof Query) { + query = query.serialize(); + } + + return compileAndRunAqlQuery(schema, schemaConfig, query, { + params, + executors: schemaExecutors, + }); +} diff --git a/packages/loot-core/src/server/aql/schema/executors.test.ts b/packages/loot-core/src/server/aql/schema/executors.test.ts index b05a8db0a3..e430dcca20 100644 --- a/packages/loot-core/src/server/aql/schema/executors.test.ts +++ b/packages/loot-core/src/server/aql/schema/executors.test.ts @@ -2,6 +2,7 @@ import { setClock } from '@actual-app/crdt'; import fc from 'fast-check'; +import { aqlQuery } from '..'; import * as arbs from '../../../mocks/arbitrary-schema'; import { q } from '../../../shared/query'; import { groupById } from '../../../shared/util'; @@ -9,7 +10,6 @@ import * as db from '../../db'; import { batchMessages, setSyncingMode } from '../../sync/index'; import { isHappyPathQuery } from './executors'; -import { runQuery } from './run-query'; beforeEach(global.emptyDatabase()); @@ -107,7 +107,7 @@ async function expectPagedData(query, numTransactions, allData) { expect(i).toBeLessThanOrEqual(100); // Pull in all the data via pages - const { data } = await runQuery( + const { data } = await aqlQuery( query.limit(pageCount).offset(pagedData.length).serialize(), ); @@ -139,7 +139,7 @@ describe('transaction executors', () => { async arr => { await insertTransactions(arr); - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ amount: { $lt: 0 } }) .select('*') @@ -150,7 +150,7 @@ describe('transaction executors', () => { expect(data.filter(t => t.is_parent).length).toBe(0); expect(data.filter(t => t.tombstone).length).toBe(0); - const { data: defaultData } = await runQuery( + const { data: defaultData } = await aqlQuery( q('transactions') .filter({ amount: { $lt: 0 } }) .select('*') @@ -176,7 +176,7 @@ describe('transaction executors', () => { async arr => { await insertTransactions(arr); - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .filter({ amount: { $lt: 0 } }) .select('*') @@ -209,7 +209,7 @@ describe('transaction executors', () => { .options({ splits: 'grouped' }) .calculate({ $sum: '$amount' }); - const { data } = await runQuery(aggQuery.serialize()); + const { data } = await aqlQuery(aggQuery.serialize()); const sum = aliveTransactions(arr).reduce((sum, trans) => { const amount = trans.amount || 0; @@ -253,14 +253,14 @@ describe('transaction executors', () => { // First to a query without order to make sure the default // order works - const { data: defaultOrderData } = await runQuery(query.serialize()); + const { data: defaultOrderData } = await aqlQuery(query.serialize()); expectTransactionOrder(defaultOrderData); expect(new Set(defaultOrderData.map(t => t.id))).toEqual(expectedIds); // Now do the full test, and add a custom order to make // sure that doesn't effect anything const orderedQuery = query.orderBy(orderFields); - const { data } = await runQuery(orderedQuery.serialize()); + const { data } = await aqlQuery(orderedQuery.serialize()); expect(new Set(data.map(t => t.id))).toEqual(expectedIds); // Validate paging and ordering diff --git a/packages/loot-core/src/server/aql/schema/executors.ts b/packages/loot-core/src/server/aql/schema/executors.ts index 50e3c30d56..b4b90cd4fb 100644 --- a/packages/loot-core/src/server/aql/schema/executors.ts +++ b/packages/loot-core/src/server/aql/schema/executors.ts @@ -1,17 +1,23 @@ // @ts-strict-ignore -import { q } from '../../../shared/query'; -import { CategoryEntity, CategoryGroupEntity } from '../../../types/models'; +import { aqlQuery } from '..'; +import { q, QueryState } from '../../../shared/query'; +import { CategoryEntity } from '../../../types/models'; import * as db from '../../db'; import { whereIn } from '../../db/util'; -import { isAggregateQuery } from '../compiler'; -import { execQuery } from '../exec'; +import { + CompilerState, + isAggregateQuery, + OutputTypes, + SqlPieces, +} from '../compiler'; +import { AqlQueryExecutor, execQuery } from '../exec'; import { convertOutputType } from '../schema-helpers'; -import { runQuery as aqlQuery } from './run-query'; - // Transactions executor +type SplitsOption = 'all' | 'inline' | 'none' | 'grouped'; + function toGroup(parents, children, mapper = x => x) { return parents.reduce((list, parent) => { const childs = children.get(parent.id) || []; @@ -32,30 +38,36 @@ function toGroup(parents, children, mapper = x => x) { // transactions, and the second will return the count of all parent // (or non-split) transactions -function execTransactions(state, query, sql, params, outputTypes) { - const tableOptions = query.tableOptions || {}; - const splitType = tableOptions.splits || 'inline'; - - if (['all', 'inline', 'none', 'grouped'].indexOf(splitType) === -1) { +function execTransactions( + compilerState: CompilerState, + queryState: QueryState, + sqlPieces: SqlPieces, + params: (string | number)[], + outputTypes: OutputTypes, +) { + const tableOptions = queryState.tableOptions || {}; + const splitType = tableOptions.splits + ? (tableOptions.splits as string) + : 'inline'; + if (!isValidSplitsOption(splitType)) { throw new Error(`Invalid “splits” option for transactions: “${splitType}”`); } if (splitType === 'all' || splitType === 'inline' || splitType === 'none') { return execTransactionsBasic( - state, - query, - sql, + compilerState, + queryState, + sqlPieces, params, splitType, outputTypes, ); } else if (splitType === 'grouped') { return execTransactionsGrouped( - state, - query, - sql, + compilerState, + queryState, + sqlPieces, params, - splitType, outputTypes, ); } @@ -81,15 +93,14 @@ export function isHappyPathQuery(queryState) { } async function execTransactionsGrouped( - state, - queryState, - sql, - params, - splitType, - outputTypes, + compilerState: CompilerState, + queryState: QueryState, + sqlPieces: SqlPieces, + params: (string | number)[], + outputTypes: OutputTypes, ) { const { withDead } = queryState; - const whereDead = withDead ? '' : `AND ${sql.from}.tombstone = 0`; + const whereDead = withDead ? '' : `AND ${sqlPieces.from}.tombstone = 0`; // Aggregate queries don't make sense for a grouped transactions // query. We never should include both parent and children @@ -97,7 +108,7 @@ async function execTransactionsGrouped( // would never make sense. In this case, switch back to the "inline" // type where only non-parent transactions are considered if (isAggregateQuery(queryState)) { - const s = { ...sql }; + const s = { ...sqlPieces }; // Modify the where to only include non-parents s.where = `${s.where} AND ${s.from}.is_parent = 0`; @@ -112,7 +123,7 @@ async function execTransactionsGrouped( s.from = 'v_transactions_internal_alive v_transactions_internal'; } - return execQuery(queryState, state, s, params, outputTypes); + return execQuery(queryState, compilerState, s, params, outputTypes); } let rows; @@ -122,13 +133,13 @@ async function execTransactionsGrouped( // This is just an optimization - we can just filter out children // directly and only list parents const rowSql = ` - SELECT ${sql.from}.id as group_id - FROM ${sql.from} - ${sql.joins} - ${sql.where} AND is_child = 0 ${whereDead} - ${sql.orderBy} - ${sql.limit != null ? `LIMIT ${sql.limit}` : ''} - ${sql.offset != null ? `OFFSET ${sql.offset}` : ''} + SELECT ${sqlPieces.from}.id as group_id + FROM ${sqlPieces.from} + ${sqlPieces.joins} + ${sqlPieces.where} AND is_child = 0 ${whereDead} + ${sqlPieces.orderBy} + ${sqlPieces.limit != null ? `LIMIT ${sqlPieces.limit}` : ''} + ${sqlPieces.offset != null ? `OFFSET ${sqlPieces.offset}` : ''} `; rows = await db.all(rowSql, params); } else { @@ -141,19 +152,19 @@ async function execTransactionsGrouped( group_id, GROUP_CONCAT(id) as matched FROM ( - SELECT ${sql.from}.id, IFNULL(${sql.from}.parent_id, ${sql.from}.id) as group_id - FROM ${sql.from} - LEFT JOIN transactions _t2 ON ${sql.from}.is_child = 1 AND _t2.id = ${sql.from}.parent_id - ${sql.joins} - ${sql.where} AND ${sql.from}.tombstone = 0 AND IFNULL(_t2.tombstone, 0) = 0 + SELECT ${sqlPieces.from}.id, IFNULL(${sqlPieces.from}.parent_id, ${sqlPieces.from}.id) as group_id + FROM ${sqlPieces.from} + LEFT JOIN transactions _t2 ON ${sqlPieces.from}.is_child = 1 AND _t2.id = ${sqlPieces.from}.parent_id + ${sqlPieces.joins} + ${sqlPieces.where} AND ${sqlPieces.from}.tombstone = 0 AND IFNULL(_t2.tombstone, 0) = 0 ) GROUP BY group_id ) - LEFT JOIN ${sql.from} ON ${sql.from}.id = group_id - ${sql.joins} - ${sql.orderBy} - ${sql.limit != null ? `LIMIT ${sql.limit}` : ''} - ${sql.offset != null ? `OFFSET ${sql.offset}` : ''} + LEFT JOIN ${sqlPieces.from} ON ${sqlPieces.from}.id = group_id + ${sqlPieces.joins} + ${sqlPieces.orderBy} + ${sqlPieces.limit != null ? `LIMIT ${sqlPieces.limit}` : ''} + ${sqlPieces.offset != null ? `OFFSET ${sqlPieces.offset}` : ''} `; rows = await db.all(rowSql, params); @@ -167,13 +178,13 @@ async function execTransactionsGrouped( const where = whereIn( rows.map(row => row.group_id), - `IFNULL(${sql.from}.parent_id, ${sql.from}.id)`, + `IFNULL(${sqlPieces.from}.parent_id, ${sqlPieces.from}.id)`, ); const finalSql = ` - SELECT ${sql.select}, parent_id AS _parent_id FROM ${sql.from} - ${sql.joins} + SELECT ${sqlPieces.select}, parent_id AS _parent_id FROM ${sqlPieces.from} + ${sqlPieces.joins} WHERE ${where} ${whereDead} - ${sql.orderBy} + ${sqlPieces.orderBy} `; const allRows = await db.all< @@ -215,14 +226,14 @@ async function execTransactionsGrouped( } async function execTransactionsBasic( - state, - queryState, - sql, - params, - splitType, - outputTypes, + compilerState: CompilerState, + queryState: QueryState, + sqlPieces: SqlPieces, + params: (string | number)[], + splitType: SplitsOption, + outputTypes: OutputTypes, ) { - const s = { ...sql }; + const s = { ...sqlPieces }; if (splitType !== 'all') { if (splitType === 'none') { @@ -232,41 +243,68 @@ async function execTransactionsBasic( } } - return execQuery(queryState, state, s, params, outputTypes); + return execQuery(queryState, compilerState, s, params, outputTypes); } -async function execCategoryGroups(state, query, sql, params, outputTypes) { - const tableOptions = query.tableOptions || {}; - const categoriesOption = tableOptions.categories || 'all'; +function isValidSplitsOption(splits: string): splits is SplitsOption { + return ['all', 'inline', 'none', 'grouped'].includes(splits); +} + +// Category groups executor + +type CategoriesOption = 'all' | 'none'; + +async function execCategoryGroups( + compilerState: CompilerState, + queryState: QueryState, + sqlPieces: SqlPieces, + params: (string | number)[], + outputTypes: OutputTypes, +) { + const tableOptions = queryState.tableOptions || {}; + const categoriesOption = tableOptions.categories + ? (tableOptions.categories as string) + : 'all'; + if (!isValidCategoriesOption(categoriesOption)) { + throw new Error( + `Invalid “categories” option for category_groups: “${categoriesOption}”`, + ); + } if (categoriesOption !== 'none') { return execCategoryGroupsWithCategories( - state, - query, - sql, + compilerState, + queryState, + sqlPieces, params, categoriesOption, outputTypes, ); } - return execCategoryGroupsBasic(state, query, sql, params, outputTypes); + return execCategoryGroupsBasic( + compilerState, + queryState, + sqlPieces, + params, + outputTypes, + ); } async function execCategoryGroupsWithCategories( - state, - queryState, - sql, - params, - categoriesOption, - outputTypes, + compilerState: CompilerState, + queryState: QueryState, + sqlPieces: SqlPieces, + params: (string | number)[], + categoriesOption: CategoriesOption, + outputTypes: OutputTypes, ) { - const categoryGroups = (await execCategoryGroupsBasic( - state, + const categoryGroups = await execCategoryGroupsBasic( + compilerState, queryState, - sql, + sqlPieces, params, outputTypes, - )) as CategoryGroupEntity[]; + ); if (categoriesOption === 'none') { return categoryGroups; @@ -290,16 +328,22 @@ async function execCategoryGroupsWithCategories( } async function execCategoryGroupsBasic( - state, - queryState, - sql, - params, - outputTypes, + compilerState: CompilerState, + queryState: QueryState, + sqlPieces: SqlPieces, + params: (string | number)[], + outputTypes: OutputTypes, ) { - return execQuery(queryState, state, sql, params, outputTypes); + return execQuery(queryState, compilerState, sqlPieces, params, outputTypes); } -export const schemaExecutors = { +function isValidCategoriesOption( + categories: string, +): categories is CategoriesOption { + return ['all', 'none'].includes(categories); +} + +export const schemaExecutors: Record = { transactions: execTransactions, category_groups: execCategoryGroups, }; diff --git a/packages/loot-core/src/server/aql/schema/run-query.ts b/packages/loot-core/src/server/aql/schema/run-query.ts deleted file mode 100644 index 89fa346561..0000000000 --- a/packages/loot-core/src/server/aql/schema/run-query.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-strict-ignore -import { Query, type QueryState } from '../../../shared/query'; -import { - runQuery as _runQuery, - runCompiledQuery as _runCompiledQuery, -} from '../exec'; - -import { schemaExecutors } from './executors'; - -import { schema, schemaConfig } from './index'; - -export function runCompiledQuery(query, sqlPieces, state, params?: unknown) { - return _runCompiledQuery(query, sqlPieces, state, { - params, - executors: schemaExecutors, - }); -} - -export function runQuery(query: Query | QueryState, params?: unknown) { - if (query instanceof Query) { - query = query.serialize(); - } - - return _runQuery(schema, schemaConfig, query, { - params, - executors: schemaExecutors, - }); -} diff --git a/packages/loot-core/src/server/budget/app.ts b/packages/loot-core/src/server/budget/app.ts index 2ab2a6dcb9..fdfe7d46f9 100644 --- a/packages/loot-core/src/server/budget/app.ts +++ b/packages/loot-core/src/server/budget/app.ts @@ -2,7 +2,7 @@ import * as monthUtils from '../../shared/months'; import { q } from '../../shared/query'; import { CategoryEntity, CategoryGroupEntity } from '../../types/models'; import { createApp } from '../app'; -import { runQuery as aqlQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import { APIError } from '../errors'; import { categoryGroupModel, categoryModel } from '../models'; diff --git a/packages/loot-core/src/server/budget/base.ts b/packages/loot-core/src/server/budget/base.ts index d184542cc2..93c44e3225 100644 --- a/packages/loot-core/src/server/budget/base.ts +++ b/packages/loot-core/src/server/budget/base.ts @@ -3,7 +3,7 @@ import * as monthUtils from '../../shared/months'; import { q } from '../../shared/query'; import { getChangedValues } from '../../shared/util'; import { CategoryGroupEntity } from '../../types/models'; -import { runQuery as aqlQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import * as sheet from '../sheet'; import { resolveName } from '../spreadsheet/util'; diff --git a/packages/loot-core/src/server/dashboard/app.ts b/packages/loot-core/src/server/dashboard/app.ts index da9564f2da..8fc7258248 100644 --- a/packages/loot-core/src/server/dashboard/app.ts +++ b/packages/loot-core/src/server/dashboard/app.ts @@ -12,7 +12,7 @@ import { } from '../../types/models'; import { type EverythingButIdOptional } from '../../types/util'; import { createApp } from '../app'; -import { runQuery as aqlQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import { ValidationError } from '../errors'; import { requiredFields } from '../models'; diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 33c4c440cc..f3160724d3 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -13,7 +13,7 @@ import { Handlers } from '../types/handlers'; import { app as accountsApp } from './accounts/app'; import { app as adminApp } from './admin/app'; import { installAPI } from './api'; -import { runQuery as aqlQuery } from './aql'; +import { aqlQuery } from './aql'; import { app as authApp } from './auth/app'; import { app as budgetApp } from './budget/app'; import { app as budgetFilesApp } from './budgetfiles/app'; diff --git a/packages/loot-core/src/server/schedules/app.test.ts b/packages/loot-core/src/server/schedules/app.test.ts index a46ea12de0..25230c90b0 100644 --- a/packages/loot-core/src/server/schedules/app.test.ts +++ b/packages/loot-core/src/server/schedules/app.test.ts @@ -3,7 +3,7 @@ import MockDate from 'mockdate'; import { q } from '../../shared/query'; import { getNextDate } from '../../shared/schedules'; -import { runQuery as aqlQuery } from '../aql'; +import { aqlQuery } from '../aql'; import { loadMappings } from '../db/mappings'; import { loadRules, updateRule } from '../transactions/transaction-rules'; diff --git a/packages/loot-core/src/server/schedules/app.ts b/packages/loot-core/src/server/schedules/app.ts index ec27512393..7501a28b13 100644 --- a/packages/loot-core/src/server/schedules/app.ts +++ b/packages/loot-core/src/server/schedules/app.ts @@ -19,7 +19,7 @@ import { import { ScheduleEntity } from '../../types/models'; import { addTransactions } from '../accounts/sync'; import { createApp } from '../app'; -import { runQuery as aqlQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import { toDateRepr } from '../models'; import { mutator, runMutator } from '../mutators'; diff --git a/packages/loot-core/src/server/schedules/find-schedules.ts b/packages/loot-core/src/server/schedules/find-schedules.ts index 3605e17735..5197f8a8c8 100644 --- a/packages/loot-core/src/server/schedules/find-schedules.ts +++ b/packages/loot-core/src/server/schedules/find-schedules.ts @@ -7,7 +7,7 @@ import { q } from '../../shared/query'; import { getApproxNumberThreshold } from '../../shared/rules'; import { recurConfigToRSchedule } from '../../shared/schedules'; import { groupBy } from '../../shared/util'; -import { runQuery as aqlQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import { fromDateRepr } from '../models'; import { conditionsToAQL } from '../transactions/transaction-rules'; diff --git a/packages/loot-core/src/server/spreadsheet/spreadsheet.ts b/packages/loot-core/src/server/spreadsheet/spreadsheet.ts index 27fb5418ca..c85a4e7486 100644 --- a/packages/loot-core/src/server/spreadsheet/spreadsheet.ts +++ b/packages/loot-core/src/server/spreadsheet/spreadsheet.ts @@ -2,7 +2,7 @@ import mitt from 'mitt'; import { QueryState } from '../../shared/query'; -import { compileQuery, runCompiledQuery, schema, schemaConfig } from '../aql'; +import { compileQuery, aqlCompiledQuery, schema, schemaConfig } from '../aql'; import { BudgetType } from '../prefs'; import { Graph } from './graph-data-structure'; @@ -169,7 +169,7 @@ export class Spreadsheet { ); } } else if (node.sql) { - result = runCompiledQuery( + result = aqlCompiledQuery( node.query, node.sql.sqlPieces, node.sql.state, diff --git a/packages/loot-core/src/server/tools/app.ts b/packages/loot-core/src/server/tools/app.ts index 6de36c68c4..ab664dd30e 100644 --- a/packages/loot-core/src/server/tools/app.ts +++ b/packages/loot-core/src/server/tools/app.ts @@ -2,7 +2,7 @@ import { q } from '../../shared/query'; import { TransactionEntity } from '../../types/models'; import { createApp } from '../app'; -import { runQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import { runMutator } from '../mutators'; import { batchUpdateTransactions } from '../transactions'; @@ -76,7 +76,7 @@ async function fixSplitTransactions(): Promise<{ }); const splitTransactions = ( - await runQuery( + await aqlQuery( q('transactions') .options({ splits: 'grouped' }) .filter({ diff --git a/packages/loot-core/src/server/transactions/app.ts b/packages/loot-core/src/server/transactions/app.ts index 6596d4f38b..28654fc573 100644 --- a/packages/loot-core/src/server/transactions/app.ts +++ b/packages/loot-core/src/server/transactions/app.ts @@ -6,7 +6,7 @@ import { TransactionEntity, } from '../../types/models'; import { createApp } from '../app'; -import { runQuery } from '../aql'; +import { aqlQuery } from '../aql'; import { mutator } from '../mutators'; import { undoable } from '../undo'; @@ -92,7 +92,7 @@ async function exportTransactionsQuery({ } async function getEarliestTransaction() { - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions') .options({ splits: 'none' }) .orderBy({ date: 'asc' }) diff --git a/packages/loot-core/src/server/transactions/export/export-to-csv.ts b/packages/loot-core/src/server/transactions/export/export-to-csv.ts index 7035069fd1..50f9644e8c 100644 --- a/packages/loot-core/src/server/transactions/export/export-to-csv.ts +++ b/packages/loot-core/src/server/transactions/export/export-to-csv.ts @@ -2,7 +2,7 @@ import csvStringify from 'csv-stringify/lib/sync'; import { integerToAmount } from '../../../shared/util'; -import { runQuery as aqlQuery } from '../../aql'; +import { aqlQuery } from '../../aql'; export async function exportToCSV( transactions, diff --git a/packages/loot-core/src/server/transactions/transaction-rules.test.ts b/packages/loot-core/src/server/transactions/transaction-rules.test.ts index a6827c05bc..a4241ffcb6 100644 --- a/packages/loot-core/src/server/transactions/transaction-rules.test.ts +++ b/packages/loot-core/src/server/transactions/transaction-rules.test.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore import { q } from '../../shared/query'; -import { runQuery } from '../aql'; +import { aqlQuery } from '../aql'; import * as db from '../db'; import { loadMappings } from '../db/mappings'; @@ -29,7 +29,7 @@ beforeEach(async () => { async function getMatchingTransactions(conds) { const { filters } = conditionsToAQL(conds); - const { data } = await runQuery( + const { data } = await aqlQuery( q('transactions').filter({ $and: filters }).select('*'), ); return data; diff --git a/upcoming-release-notes/4787.md b/upcoming-release-notes/4787.md new file mode 100644 index 0000000000..f27470134f --- /dev/null +++ b/upcoming-release-notes/4787.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Rename AQL module's runQuery function to aqlQuery to disambiguate with DB module runQuery function