diff --git a/packages/loot-core/src/server/importers/ynab4.ts b/packages/loot-core/src/server/importers/ynab4.ts index 7f237b500e..540ad9dfb6 100644 --- a/packages/loot-core/src/server/importers/ynab4.ts +++ b/packages/loot-core/src/server/importers/ynab4.ts @@ -1,10 +1,4 @@ // @ts-strict-ignore -// This is a special usage of the API because this package is embedded -// into Actual itself. We only want to pull in the methods in that -// case and ignore everything else; otherwise we'd be pulling in the -// entire backend bundle from the API -import { send } from '@actual-app/api/injected'; -import * as actual from '@actual-app/api/methods'; import AdmZip from 'adm-zip'; import normalizePathSep from 'slash'; import { v4 as uuidv4 } from 'uuid'; @@ -12,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { logger } from '../../platform/server/log'; import * as monthUtils from '../../shared/months'; import { amountToInteger, groupBy, sortByKey } from '../../shared/util'; +import { send } from '../main-app'; import type * as YNAB4 from './ynab4-types'; @@ -26,10 +21,12 @@ async function importAccounts( return Promise.all( accounts.map(async account => { if (!account.isTombstone) { - const id = await actual.createAccount({ - name: account.accountName, - offbudget: account.onBudget ? false : true, - closed: account.hidden ? true : false, + const id = await send('api/account-create', { + account: { + name: account.accountName, + offbudget: account.onBudget ? false : true, + closed: account.hidden ? true : false, + }, }); entityIdMap.set(account.entityId, id); } @@ -51,13 +48,18 @@ async function importCategories( masterCategory.subCategories && masterCategory.subCategories.some(cat => !cat.isTombstone) ) { - const id = await actual.createCategoryGroup({ - name: masterCategory.name, - is_income: false, + const id = await send('api/category-group-create', { + group: { + name: masterCategory.name, + is_income: false, + }, }); entityIdMap.set(masterCategory.entityId, id); if (masterCategory.note) { - send('notes-save', { id, note: masterCategory.note }); + void send('notes-save', { + id, + note: masterCategory.note, + }); } if (masterCategory.subCategories) { @@ -87,13 +89,18 @@ async function importCategories( categoryName = categoryNameParts.join('/').trim(); } - const id = await actual.createCategory({ - name: categoryName, - group_id: entityIdMap.get(category.masterCategoryId), + const id = await send('api/category-create', { + category: { + name: categoryName, + group_id: entityIdMap.get(category.masterCategoryId), + }, }); entityIdMap.set(category.entityId, id); if (category.note) { - send('notes-save', { id, note: category.note }); + void send('notes-save', { + id, + note: category.note, + }); } } } @@ -109,9 +116,11 @@ async function importPayees( ) { for (const payee of data.payees) { if (!payee.isTombstone) { - const id = await actual.createPayee({ - name: payee.name, - transfer_acct: entityIdMap.get(payee.targetAccountId) || null, + const id = await send('api/payee-create', { + payee: { + name: payee.name, + transfer_acct: entityIdMap.get(payee.targetAccountId) || null, + }, }); // TODO: import payee rules @@ -125,12 +134,14 @@ async function importTransactions( data: YNAB4.YFull, entityIdMap: Map, ) { - const categories = await actual.getCategories(); + const categories = await send('api/categories-get', { + grouped: false, + }); const incomeCategoryId: string = categories.find( cat => cat.name === 'Income', ).id; - const accounts = await actual.getAccounts(); - const payees = await actual.getPayees(); + const accounts = await send('api/accounts-get'); + const payees = await send('api/payees-get'); function getCategory(id: string) { if (id == null || id === 'Category/__Split__') { @@ -234,8 +245,11 @@ async function importTransactions( }) .filter(x => x); - await actual.addTransactions(entityIdMap.get(accountId), toImport, { + await send('api/transactions-add', { + accountId: entityIdMap.get(accountId), + transactions: toImport, learnCategories: true, + runTransfers: false, }); }), ); @@ -277,7 +291,8 @@ async function importBudgets( ) { const budgets = sortByKey(data.monthlyBudgets, 'month'); - await actual.batchBudgetUpdates(async () => { + await send('api/batch-budget-start'); + try { for (const budget of budgets) { const filled = fillInBudgets( data, @@ -293,17 +308,31 @@ async function importBudgets( return; } - await actual.setBudgetAmount(month, catId, amount); + await send('api/budget-set-amount', { + month, + categoryId: catId, + amount, + }); if (catBudget.overspendingHandling === 'AffectsBuffer') { - await actual.setBudgetCarryover(month, catId, false); + await send('api/budget-set-carryover', { + month, + categoryId: catId, + flag: false, + }); } else if (catBudget.overspendingHandling === 'Confined') { - await actual.setBudgetCarryover(month, catId, true); + await send('api/budget-set-carryover', { + month, + categoryId: catId, + flag: true, + }); } }), ); } - }); + } finally { + await send('api/batch-budget-end'); + } } function estimateRecentness(str: string) { diff --git a/packages/loot-core/src/server/importers/ynab5.ts b/packages/loot-core/src/server/importers/ynab5.ts index de2df97e73..e3a98baf48 100644 --- a/packages/loot-core/src/server/importers/ynab5.ts +++ b/packages/loot-core/src/server/importers/ynab5.ts @@ -1,10 +1,4 @@ // @ts-strict-ignore -// This is a special usage of the API because this package is embedded -// into Actual itself. We only want to pull in the methods in that -// case and ignore everything else; otherwise we'd be pulling in the -// entire backend bundle from the API -import { send } from '@actual-app/api/injected'; -import * as actual from '@actual-app/api/methods'; import { v4 as uuidv4 } from 'uuid'; import { logger } from '../../platform/server/log'; @@ -12,6 +6,7 @@ import * as monthUtils from '../../shared/months'; import { q } from '../../shared/query'; import { groupBy, sortByKey } from '../../shared/util'; import type { RecurConfig, RecurPattern, RuleEntity } from '../../types/models'; +import { send } from '../main-app'; import { ruleModel } from '../transactions/transaction-rules'; import type { @@ -276,10 +271,12 @@ function importAccounts(data: Budget, entityIdMap: Map) { return Promise.all( data.accounts.map(async account => { if (!account.deleted) { - const id = await actual.createAccount({ - name: account.name, - offbudget: account.on_budget ? false : true, - closed: account.closed, + const id = await send('api/account-create', { + account: { + name: account.name, + offbudget: account.on_budget ? false : true, + closed: account.closed, + }, }); entityIdMap.set(account.id, id); } @@ -294,7 +291,9 @@ async function importCategories( // Hidden categories are put in its own group by YNAB, // so it's already handled. - const categories = await actual.getCategories(); + const categories = await send('api/categories-get', { + grouped: false, + }); const incomeCatId = findIdByName(categories, 'Income'); const ynabIncomeCategories = ['To be Budgeted', 'Inflow: Ready to Assign']; @@ -337,7 +336,9 @@ async function importCategories( while (true) { const name = count === 0 ? baseName : `${baseName} (${count})`; try { - const id = await actual.createCategoryGroup({ ...params, name }); + const id = await send('api/category-group-create', { + group: { ...params, name }, + }); return { id, name }; } catch (e) { if (count >= MAX_RETRY) { @@ -360,7 +361,9 @@ async function importCategories( while (true) { const name = count === 0 ? baseName : `${baseName} (${count})`; try { - const id = await actual.createCategory({ ...params, name }); + const id = await send('api/category-create', { + category: { ...params, name }, + }); return { id, name }; } catch (e) { if (count >= MAX_RETRY) { @@ -390,7 +393,10 @@ async function importCategories( groupId = createdGroup.id; entityIdMap.set(group.id, groupId); if (group.note) { - send('notes-save', { id: groupId, note: group.note }); + void send('notes-save', { + id: groupId, + note: group.note, + }); } } @@ -428,7 +434,7 @@ async function importCategories( }); entityIdMap.set(cat.id, createdCategory.id); if (cat.note) { - send('notes-save', { + void send('notes-save', { id: createdCategory.id, note: cat.note, }); @@ -445,8 +451,8 @@ function importPayees(data: Budget, entityIdMap: Map) { return Promise.all( data.payees.map(async payee => { if (!payee.deleted) { - const id = await actual.createPayee({ - name: payee.name, + const id = await send('api/payee-create', { + payee: { name: payee.name }, }); entityIdMap.set(payee.id, id); } @@ -510,8 +516,10 @@ async function importTransactions( entityIdMap: Map, flagNameConflicts: Set, ) { - const payees = await actual.getPayees(); - const categories = await actual.getCategories(); + const payees = await send('api/payees-get'); + const categories = await send('api/categories-get', { + grouped: false, + }); const incomeCatId = findIdByName(categories, 'Income'); const startingBalanceCatId = findIdByName(categories, 'Starting Balances'); //better way to do it? @@ -774,8 +782,11 @@ async function importTransactions( }) .filter(x => x); - await actual.addTransactions(entityIdMap.get(accountId), toImport, { + await send('api/transactions-add', { + accountId: entityIdMap.get(accountId), + transactions: toImport, learnCategories: true, + runTransfers: false, }); }), ); @@ -795,7 +806,7 @@ async function importScheduledTransactions( return; } - const payees = await actual.getPayees(); + const payees = await send('api/payees-get'); const payeesByTransferAcct = payees .filter(payee => payee?.transfer_acct) .map(payee => [payee.transfer_acct, payee] as [string, Payee]); @@ -818,7 +829,10 @@ async function importScheduledTransactions( while (true) { try { - return await actual.createSchedule({ ...params, name: params.name }); + return await send('api/schedule-create', { + ...params, + name: params.name, + }); } catch (e) { if (count >= MAX_RETRY) { const errorMsg = normalizeError(e); @@ -833,16 +847,19 @@ async function importScheduledTransactions( async function getRuleForSchedule( scheduleId: string, ): Promise { - const { data: ruleId } = (await actual.aqlQuery( - q('schedules').filter({ id: scheduleId }).calculate('rule'), - )) as { data: string | null }; + const { data: ruleId } = (await send('api/query', { + query: q('schedules') + .filter({ id: scheduleId }) + .calculate('rule') + .serialize(), + })) as { data: string | null }; if (!ruleId) { return null; } - const { data: ruleData } = (await actual.aqlQuery( - q('rules').filter({ id: ruleId }).select('*'), - )) as { data: Array> }; + const { data: ruleData } = (await send('api/query', { + query: q('rules').filter({ id: ruleId }).select('*').serialize(), + })) as { data: Array> }; const ruleRow = ruleData?.[0]; if (!ruleRow) { return null; @@ -901,7 +918,9 @@ async function importScheduledTransactions( value: scheduleNotes, }); - await actual.updateRule(buildRuleUpdate(rule, actions)); + await send('api/rule-update', { + rule: buildRuleUpdate(rule, actions), + }); } } @@ -934,7 +953,9 @@ async function importScheduledTransactions( value: categoryId, }); - await actual.updateRule(buildRuleUpdate(rule, actions)); + await send('api/rule-update', { + rule: buildRuleUpdate(rule, actions), + }); } for (const [scheduleId, subtransactions] of scheduleSplitsMap.entries()) { @@ -1011,7 +1032,9 @@ async function importScheduledTransactions( } }); - await actual.updateRule(buildRuleUpdate(rule, actions)); + await send('api/rule-update', { + rule: buildRuleUpdate(rule, actions), + }); } } } @@ -1036,7 +1059,8 @@ async function importBudgets(data: Budget, entityIdMap: Map) { 'Credit Card Payments', ); - await actual.batchBudgetUpdates(async () => { + await send('api/batch-budget-start'); + try { for (const budget of budgets) { const month = monthUtils.monthFromDate(budget.month); @@ -1053,11 +1077,17 @@ async function importBudgets(data: Budget, entityIdMap: Map) { return; } - await actual.setBudgetAmount(month, catId, amount); + await send('api/budget-set-amount', { + month, + categoryId: catId, + amount, + }); }), ); } - }); + } finally { + await send('api/batch-budget-end'); + } } export function parseFile(buffer: Buffer): Budget { diff --git a/packages/loot-core/src/server/main-app.ts b/packages/loot-core/src/server/main-app.ts index 07b2b2fb20..d51bd57c6c 100644 --- a/packages/loot-core/src/server/main-app.ts +++ b/packages/loot-core/src/server/main-app.ts @@ -2,6 +2,7 @@ import * as connection from '../platform/server/connection'; import type { Handlers } from '../types/handlers'; import { createApp } from './app'; +import { runHandler } from './mutators'; // Main app export const app = createApp(); @@ -9,3 +10,16 @@ export const app = createApp(); app.events.on('sync', event => { connection.send('sync-event', event); }); + +/** + * Run a handler by name (server-side). Same API shape as the client connection's send. + * Used by server code that needs to invoke handlers directly, e.g. importers. + */ +export async function send( + name: K, + args?: Parameters[0], +): Promise>> { + return runHandler(app.handlers[name], args, { name }) as Promise< + Awaited> + >; +} diff --git a/upcoming-release-notes/7050.md b/upcoming-release-notes/7050.md new file mode 100644 index 0000000000..9c8a58cdfb --- /dev/null +++ b/upcoming-release-notes/7050.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Remove reliance on the API package in YNAB importers