mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-21 06:58:47 -05:00
Compare commits
4 Commits
js-proxy
...
claude/bro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
645623a6c9 | ||
|
|
1442747896 | ||
|
|
3e2303e5dc | ||
|
|
332db28e2e |
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Run yarn install after pulling/merging (if yarn.lock changed)
|
||||
|
||||
if git diff --name-only ORIG_HEAD HEAD | grep -q "^yarn.lock$"; then
|
||||
echo "yarn.lock changed — running yarn install..."
|
||||
yarn install
|
||||
fi
|
||||
25
packages/api/index.web.ts
Normal file
25
packages/api/index.web.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { init as initLootCore } from '@actual-app/core/server/main';
|
||||
import type { InitConfig, lib } from '@actual-app/core/server/main';
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
let internal: typeof lib | null = null;
|
||||
|
||||
export async function init(config: InitConfig = {}) {
|
||||
internal = await initLootCore(config);
|
||||
return internal;
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (internal) {
|
||||
try {
|
||||
await internal.send('sync');
|
||||
} catch {
|
||||
// most likely that no budget is loaded, so the sync failed
|
||||
}
|
||||
|
||||
await internal.send('close-budget');
|
||||
internal = null;
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,39 @@
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/browser.js",
|
||||
"types": "@types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/browser.js"
|
||||
},
|
||||
"default": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/browser.js"
|
||||
},
|
||||
"default": {
|
||||
"types": "./@types/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build": "yarn build:node && yarn build:browser",
|
||||
"build:node": "vite build",
|
||||
"build:browser": "vite build --config vite.browser.config.ts",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsgo -b && tsc-strict"
|
||||
},
|
||||
|
||||
@@ -18,5 +18,12 @@
|
||||
},
|
||||
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts", "*.config.ts"]
|
||||
"exclude": [
|
||||
"**/node_modules/*",
|
||||
"dist",
|
||||
"@types",
|
||||
"*.test.ts",
|
||||
"*.config.ts",
|
||||
"*.browser.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
26
packages/api/vite.browser.config.ts
Normal file
26
packages/api/vite.browser.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import path from 'path';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
|
||||
const distDir = path.resolve(__dirname, 'dist');
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'esnext',
|
||||
outDir: distDir,
|
||||
emptyOutDir: false,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'index.web.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'browser.js',
|
||||
},
|
||||
},
|
||||
plugins: [peggyLoader()],
|
||||
resolve: {
|
||||
// Default extensions — picks up browser implementations (index.ts)
|
||||
// instead of .api.ts (which resolves to Node.js/Electron code)
|
||||
extensions: ['.js', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
});
|
||||
@@ -304,7 +304,7 @@ export function registerQueryCommand(program: Command) {
|
||||
? buildQueryFromFile(parsed, cmdOpts.table)
|
||||
: buildQueryFromFlags(cmdOpts);
|
||||
|
||||
const result = await api.aqlQuery(queryObj);
|
||||
const result = (await api.aqlQuery(queryObj)) as { data: unknown };
|
||||
|
||||
if (cmdOpts.count) {
|
||||
printOutput({ count: result.data }, opts.format);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Text } from '@actual-app/components/text';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import { send, server } from 'loot-core/platform/client/connection';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import * as undo from 'loot-core/platform/client/undo';
|
||||
import { getNormalisedString } from 'loot-core/shared/normalisation';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
@@ -176,7 +176,7 @@ export function ManageRules({
|
||||
|
||||
let loadedRules = null;
|
||||
if (payeeId) {
|
||||
loadedRules = await server.getPayeeRules({
|
||||
loadedRules = await send('payees-get-rules', {
|
||||
id: payeeId,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Text } from '@actual-app/components/text';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import { server } from 'loot-core/platform/client/connection';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import type { PayeeEntity } from 'loot-core/types/models';
|
||||
|
||||
import { MobileBackButton } from '@desktop-client/components/mobile/MobileBackButton';
|
||||
@@ -58,7 +58,7 @@ export function MobilePayeeEditPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
await server.batchChangePayees({
|
||||
await send('payees-batch-change', {
|
||||
updated: [{ id: payee.id, name: editedPayeeName.trim() }],
|
||||
});
|
||||
showUndoNotification({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import { server } from 'loot-core/platform/client/connection';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import { getNormalisedString } from 'loot-core/shared/normalisation';
|
||||
import type { PayeeEntity, RuleEntity } from 'loot-core/types/models';
|
||||
|
||||
@@ -52,7 +52,7 @@ export function MobilePayeesPage() {
|
||||
// View associated rules for the payee
|
||||
if ((ruleCounts.get(payee.id) ?? 0) > 0) {
|
||||
try {
|
||||
const associatedRules: RuleEntity[] = await server.getPayeeRules({
|
||||
const associatedRules: RuleEntity[] = await send('payees-get-rules', {
|
||||
id: payee.id,
|
||||
});
|
||||
const ruleIds = associatedRules.map(rule => rule.id).join(',');
|
||||
@@ -88,7 +88,7 @@ export function MobilePayeesPage() {
|
||||
const handlePayeeDelete = useCallback(
|
||||
async (payee: PayeeEntity) => {
|
||||
try {
|
||||
await server.batchChangePayees({ deleted: [{ id: payee.id }] });
|
||||
await send('payees-batch-change', { deleted: [{ id: payee.id }] });
|
||||
showUndoNotification({
|
||||
message: t('Payee "{{name}}" deleted successfully', {
|
||||
name: payee.name,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Text } from '@actual-app/components/text';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import { send, server } from 'loot-core/platform/client/connection';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import type { PayeeEntity } from 'loot-core/types/models';
|
||||
import type { TransObjectLiteral } from 'loot-core/types/util';
|
||||
|
||||
@@ -59,7 +59,7 @@ export function MergeUnusedPayeesModal({
|
||||
|
||||
const onMerge = useCallback(
|
||||
async (targetPayee: PayeeEntity) => {
|
||||
await server.mergePayees({
|
||||
await send('payees-merge', {
|
||||
targetId: targetPayee.id,
|
||||
mergeIds: payees.map(payee => payee.id),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { listen, server } from 'loot-core/platform/client/connection';
|
||||
import { listen, send } from 'loot-core/platform/client/connection';
|
||||
import * as undo from 'loot-core/platform/client/undo';
|
||||
import type { UndoState } from 'loot-core/server/undo';
|
||||
import { applyChanges } from 'loot-core/shared/util';
|
||||
@@ -112,14 +112,14 @@ export function ManagePayeesWithData({
|
||||
orphanedPayees={orphanedPayees}
|
||||
initialSelectedIds={initialSelectedIds}
|
||||
onBatchChange={async (changes: Diff<PayeeEntity>) => {
|
||||
await server.batchChangePayees(changes);
|
||||
await send('payees-batch-change', changes);
|
||||
queryClient.setQueryData(
|
||||
payeeQueries.listOrphaned().queryKey,
|
||||
existing => applyChanges(changes, existing ?? []),
|
||||
);
|
||||
}}
|
||||
onMerge={async ([targetId, ...mergeIds]) => {
|
||||
await server.mergePayees({ targetId, mergeIds });
|
||||
await send('payees-merge', { targetId, mergeIds });
|
||||
|
||||
const targetIdIsOrphan = orphanedPayees
|
||||
.map(o => o.id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { server } from 'loot-core/platform/client/connection';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import type { LocationCoordinates } from 'loot-core/shared/location-utils';
|
||||
import type {
|
||||
NearbyPayeeEntity,
|
||||
@@ -68,7 +68,7 @@ export class SendApiLocationClient implements LocationApiClient {
|
||||
payeeId: string,
|
||||
coordinates: LocationCoordinates,
|
||||
): Promise<string> {
|
||||
return await server.createPayeeLocation({
|
||||
return await send('payee-location-create', {
|
||||
payeeId,
|
||||
latitude: coordinates.latitude,
|
||||
longitude: coordinates.longitude,
|
||||
@@ -76,18 +76,18 @@ export class SendApiLocationClient implements LocationApiClient {
|
||||
}
|
||||
|
||||
async getLocations(payeeId: string): Promise<PayeeLocationEntity[]> {
|
||||
return await server.getPayeeLocations({ payeeId });
|
||||
return await send('payee-locations-get', { payeeId });
|
||||
}
|
||||
|
||||
async deleteLocation(locationId: string): Promise<void> {
|
||||
await server.deletePayeeLocation({ id: locationId });
|
||||
await send('payee-location-delete', { id: locationId });
|
||||
}
|
||||
|
||||
async getNearbyPayees(
|
||||
coordinates: LocationCoordinates,
|
||||
maxDistance: number,
|
||||
): Promise<NearbyPayeeEntity[]> {
|
||||
const result = await server.getNearbyPayees({
|
||||
const result = await send('payees-get-nearby', {
|
||||
latitude: coordinates.latitude,
|
||||
longitude: coordinates.longitude,
|
||||
maxDistance,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { QueryClient, QueryKey } from '@tanstack/react-query';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { server } from 'loot-core/platform/client/connection';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import type { PayeeEntity } from 'loot-core/types/models';
|
||||
|
||||
import { locationService } from './location';
|
||||
@@ -99,7 +99,7 @@ export function useCreatePayeeMutation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ name }: CreatePayeePayload) => {
|
||||
const id: PayeeEntity['id'] = await server.createPayee({
|
||||
const id: PayeeEntity['id'] = await send('payee-create', {
|
||||
name: name.trim(),
|
||||
});
|
||||
return id;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { queryOptions } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
import { server } from 'loot-core/platform/client/connection';
|
||||
import { send } from 'loot-core/platform/client/connection';
|
||||
import { groupById } from 'loot-core/shared/util';
|
||||
import type {
|
||||
AccountEntity,
|
||||
@@ -21,7 +21,7 @@ export const payeeQueries = {
|
||||
queryOptions<PayeeEntity[]>({
|
||||
queryKey: [...payeeQueries.lists()],
|
||||
queryFn: async () => {
|
||||
const payees: PayeeEntity[] = (await server.getPayees()) ?? [];
|
||||
const payees: PayeeEntity[] = (await send('payees-get')) ?? [];
|
||||
return translatePayees(payees);
|
||||
},
|
||||
placeholderData: [],
|
||||
@@ -32,7 +32,7 @@ export const payeeQueries = {
|
||||
queryOptions<PayeeEntity[]>({
|
||||
queryKey: [...payeeQueries.lists(), 'common'],
|
||||
queryFn: async () => {
|
||||
const payees: PayeeEntity[] = (await server.getCommonPayees()) ?? [];
|
||||
const payees: PayeeEntity[] = (await send('common-payees-get')) ?? [];
|
||||
return translatePayees(payees);
|
||||
},
|
||||
placeholderData: [],
|
||||
@@ -44,7 +44,7 @@ export const payeeQueries = {
|
||||
queryKey: [...payeeQueries.lists(), 'orphaned'],
|
||||
queryFn: async () => {
|
||||
const payees: Pick<PayeeEntity, 'id'>[] =
|
||||
(await server.getOrphanedPayees()) ?? [];
|
||||
(await send('payees-get-orphaned')) ?? [];
|
||||
return payees;
|
||||
},
|
||||
placeholderData: [],
|
||||
@@ -55,7 +55,7 @@ export const payeeQueries = {
|
||||
queryOptions<Map<PayeeEntity['id'], number>>({
|
||||
queryKey: [...payeeQueries.lists(), 'ruleCounts'],
|
||||
queryFn: async () => {
|
||||
const counts = await server.getPayeeRuleCounts();
|
||||
const counts = await send('payees-get-rule-counts');
|
||||
return new Map(Object.entries(counts ?? {}));
|
||||
},
|
||||
placeholderData: new Map(),
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { addTransactions } from '../server/accounts/sync';
|
||||
import type { App } from '../server/app';
|
||||
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';
|
||||
import { runMutator } from '../server/mutators';
|
||||
import { runHandler, runMutator } from '../server/mutators';
|
||||
import * as sheet from '../server/sheet';
|
||||
import { batchMessages, setSyncingMode } from '../server/sync';
|
||||
import * as monthUtils from '../shared/months';
|
||||
@@ -85,6 +84,7 @@ function extractCommonThings(
|
||||
}
|
||||
|
||||
async function fillPrimaryChecking(
|
||||
handlers,
|
||||
account,
|
||||
payees: MockPayeeEntity[],
|
||||
groups: CategoryGroupEntity[],
|
||||
@@ -255,7 +255,7 @@ async function fillPrimaryChecking(
|
||||
return addTransactions(account.id, transactions);
|
||||
}
|
||||
|
||||
async function fillChecking(app, account, payees, groups) {
|
||||
async function fillChecking(handlers, account, payees, groups) {
|
||||
const { incomePayee, expensePayees, incomeGroup, expenseCategories } =
|
||||
extractCommonThings(payees, groups);
|
||||
const numTransactions = integer(20, 40);
|
||||
@@ -297,13 +297,13 @@ async function fillChecking(app, account, payees, groups) {
|
||||
starting_balance_flag: true,
|
||||
});
|
||||
|
||||
await app.runHandler('transactions-batch-update', {
|
||||
await handlers['transactions-batch-update']({
|
||||
added: transactions,
|
||||
fastMode: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function fillInvestment(app, account, payees, groups) {
|
||||
async function fillInvestment(handlers, account, payees, groups) {
|
||||
const { incomePayee, incomeGroup } = extractCommonThings(payees, groups);
|
||||
|
||||
const numTransactions = integer(10, 30);
|
||||
@@ -333,13 +333,13 @@ async function fillInvestment(app, account, payees, groups) {
|
||||
starting_balance_flag: true,
|
||||
});
|
||||
|
||||
await app.runHandler('transactions-batch-update', {
|
||||
await handlers['transactions-batch-update']({
|
||||
added: transactions,
|
||||
fastMode: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function fillSavings(app, account, payees, groups) {
|
||||
async function fillSavings(handlers, account, payees, groups) {
|
||||
const { incomePayee, expensePayees, incomeGroup, expenseCategories } =
|
||||
extractCommonThings(payees, groups);
|
||||
|
||||
@@ -378,13 +378,13 @@ async function fillSavings(app, account, payees, groups) {
|
||||
starting_balance_flag: true,
|
||||
});
|
||||
|
||||
await app.runHandler('transactions-batch-update', {
|
||||
await handlers['transactions-batch-update']({
|
||||
added: transactions,
|
||||
fastMode: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function fillMortgage(app, account, payees, groups) {
|
||||
async function fillMortgage(handlers, account, payees, groups) {
|
||||
const { incomePayee, incomeGroup } = extractCommonThings(payees, groups);
|
||||
|
||||
const numTransactions = integer(7, 10);
|
||||
@@ -415,13 +415,13 @@ async function fillMortgage(app, account, payees, groups) {
|
||||
});
|
||||
}
|
||||
|
||||
await app.runHandler('transactions-batch-update', {
|
||||
await handlers['transactions-batch-update']({
|
||||
added: transactions,
|
||||
fastMode: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function fillOther(app, account, payees, groups) {
|
||||
async function fillOther(handlers, account, payees, groups) {
|
||||
const { incomePayee, incomeGroup } = extractCommonThings(payees, groups);
|
||||
|
||||
const numTransactions = integer(3, 6);
|
||||
@@ -453,7 +453,7 @@ async function fillOther(app, account, payees, groups) {
|
||||
});
|
||||
}
|
||||
|
||||
await app.runHandler('transactions-batch-update', {
|
||||
await handlers['transactions-batch-update']({
|
||||
added: transactions,
|
||||
fastMode: true,
|
||||
});
|
||||
@@ -594,7 +594,7 @@ async function createBudget(accounts, payees, groups) {
|
||||
await sheet.waitOnSpreadsheet();
|
||||
}
|
||||
|
||||
export async function createTestBudget(app: App<Handlers>) {
|
||||
export async function createTestBudget(handlers: Handlers) {
|
||||
setSyncingMode('import');
|
||||
|
||||
db.execQuery('PRAGMA journal_mode = OFF');
|
||||
@@ -618,7 +618,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
|
||||
await runMutator(async () => {
|
||||
for (const account of accounts) {
|
||||
account.id = await app['account-create'](account);
|
||||
account.id = await handlers['account-create'](account);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -642,7 +642,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
await runMutator(() =>
|
||||
batchMessages(async () => {
|
||||
for (const newPayee of newPayees) {
|
||||
const id = await app['createPayee']({ name: newPayee.name });
|
||||
const id = await handlers['payee-create']({ name: newPayee.name });
|
||||
payees.push({
|
||||
id,
|
||||
name: newPayee.name,
|
||||
@@ -690,7 +690,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
|
||||
await runMutator(async () => {
|
||||
for (const group of newCategoryGroups) {
|
||||
const groupId = await app['category-group-create']({
|
||||
const groupId = await handlers['category-group-create']({
|
||||
name: group.name,
|
||||
isIncome: group.is_income,
|
||||
});
|
||||
@@ -702,7 +702,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
});
|
||||
|
||||
for (const category of group.categories) {
|
||||
const categoryId = await app['category-create']({
|
||||
const categoryId = await handlers['category-create']({
|
||||
...category,
|
||||
isIncome: category.is_income,
|
||||
groupId,
|
||||
@@ -717,7 +717,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
}
|
||||
});
|
||||
|
||||
const allGroups = (await app['get-categories']()).grouped;
|
||||
const allGroups = (await runHandler(handlers['get-categories'])).grouped;
|
||||
|
||||
setSyncingMode('import');
|
||||
|
||||
@@ -725,26 +725,26 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
batchMessages(async () => {
|
||||
for (const account of accounts) {
|
||||
if (account.name === 'Bank of America') {
|
||||
await fillPrimaryChecking(account, payees, allGroups);
|
||||
await fillPrimaryChecking(handlers, account, payees, allGroups);
|
||||
} else if (
|
||||
account.name === 'Capital One Checking' ||
|
||||
account.name === 'HSBC'
|
||||
) {
|
||||
await fillChecking(app, account, payees, allGroups);
|
||||
await fillChecking(handlers, account, payees, allGroups);
|
||||
} else if (account.name === 'Ally Savings') {
|
||||
await fillSavings(app, account, payees, allGroups);
|
||||
await fillSavings(handlers, account, payees, allGroups);
|
||||
} else if (
|
||||
account.name === 'Vanguard 401k' ||
|
||||
account.name === 'Roth IRA'
|
||||
) {
|
||||
await fillInvestment(app, account, payees, allGroups);
|
||||
await fillInvestment(handlers, account, payees, allGroups);
|
||||
} else if (account.name === 'Mortgage') {
|
||||
await fillMortgage(app, account, payees, allGroups);
|
||||
await fillMortgage(handlers, account, payees, allGroups);
|
||||
} else if (account.name === 'House Asset') {
|
||||
await fillOther(app, account, payees, allGroups);
|
||||
await fillOther(handlers, account, payees, allGroups);
|
||||
} else {
|
||||
console.error('Unknown account name for test budget: ', account.name);
|
||||
await fillChecking(app, account, payees, allGroups);
|
||||
await fillChecking(handlers, account, payees, allGroups);
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -773,7 +773,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
);
|
||||
const lastDeposit = results[0];
|
||||
|
||||
await app['transaction-update']({
|
||||
await runHandler(handlers['transaction-update'], {
|
||||
...lastDeposit,
|
||||
amount: lastDeposit.amount + -primaryBalance + integer(10000, 20000),
|
||||
});
|
||||
@@ -791,7 +791,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
batchMessages(async () => {
|
||||
const account = accounts.find(acc => acc.name === 'Bank of America');
|
||||
|
||||
await app['schedule/create']({
|
||||
await runHandler(handlers['schedule/create'], {
|
||||
schedule: {
|
||||
name: 'Phone bills',
|
||||
posts_transaction: false,
|
||||
@@ -822,7 +822,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
],
|
||||
});
|
||||
|
||||
await app['schedule/create']({
|
||||
await runHandler(handlers['schedule/create'], {
|
||||
schedule: {
|
||||
name: 'Internet bill',
|
||||
posts_transaction: false,
|
||||
@@ -847,7 +847,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
],
|
||||
});
|
||||
|
||||
await app['schedule/create']({
|
||||
await runHandler(handlers['schedule/create'], {
|
||||
schedule: {
|
||||
name: 'Wedding',
|
||||
posts_transaction: false,
|
||||
@@ -868,7 +868,7 @@ export async function createTestBudget(app: App<Handlers>) {
|
||||
],
|
||||
});
|
||||
|
||||
await app['schedule/create']({
|
||||
await runHandler(handlers['schedule/create'], {
|
||||
schedule: {
|
||||
name: 'Utilities',
|
||||
posts_transaction: false,
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import type { Handlers } from '../../../types/handlers';
|
||||
import type { ServerEvents } from '../../../types/server-events';
|
||||
|
||||
/**
|
||||
* Loot core server proxy.
|
||||
*/
|
||||
export type ServerProxy = {
|
||||
[K in keyof Handlers]: (
|
||||
args?: Parameters<Handlers[K]>[0],
|
||||
) => ReturnType<Handlers[K]>;
|
||||
};
|
||||
|
||||
export declare const server: ServerProxy;
|
||||
|
||||
export declare function init(): Promise<ServerProxy>;
|
||||
export declare function init(): Promise<unknown>;
|
||||
export type Init = typeof init;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,6 @@ const listeners = new Map();
|
||||
let messageQueue = [];
|
||||
|
||||
let globalWorker = null;
|
||||
let initPromise: Promise<T.ServerProxy> | null = null;
|
||||
|
||||
class ReconstructedError extends Error {
|
||||
url: string;
|
||||
@@ -84,28 +83,6 @@ function handleMessage(msg) {
|
||||
}
|
||||
}
|
||||
|
||||
export const server: T.ServerProxy = new Proxy({} as T.ServerProxy, {
|
||||
get(_target, prop: string | symbol) {
|
||||
if (typeof prop === 'symbol') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Returning undefined for 'then' prevents the proxy from being
|
||||
// treated as a thenable when awaited, which would cause Promise
|
||||
// machinery to call server.then(resolve, reject) with native functions.
|
||||
if (prop === 'then') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!initPromise) {
|
||||
throw new Error(`Cannot use server proxy before init() has been called`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (args?: any) => send(prop as any, args);
|
||||
},
|
||||
});
|
||||
|
||||
// Note that this does not support retry. If the worker
|
||||
// dies, it will permanently be disconnected. That should be OK since
|
||||
// I don't think a worker should ever die due to a system error.
|
||||
@@ -130,7 +107,7 @@ function connectWorker(worker, onOpen, onError) {
|
||||
globalWorker.postMessage({
|
||||
name: 'client-connected-to-backend',
|
||||
});
|
||||
onOpen(server);
|
||||
onOpen();
|
||||
} else if (msg.type === 'app-init-failure') {
|
||||
globalWorker.postMessage({
|
||||
name: '__app-init-failure-acknowledged',
|
||||
@@ -172,16 +149,11 @@ function connectWorker(worker, onOpen, onError) {
|
||||
}
|
||||
}
|
||||
|
||||
export const init: T.Init = function () {
|
||||
if (!initPromise) {
|
||||
initPromise = global.Actual.getServerSocket().then(
|
||||
worker =>
|
||||
new Promise((resolve, reject) =>
|
||||
connectWorker(worker, resolve, reject),
|
||||
),
|
||||
);
|
||||
}
|
||||
return initPromise;
|
||||
export const init: T.Init = async function () {
|
||||
const worker = await global.Actual.getServerSocket();
|
||||
return new Promise((resolve, reject) =>
|
||||
connectWorker(worker, resolve, reject),
|
||||
);
|
||||
};
|
||||
|
||||
export const send: T.Send = function (
|
||||
|
||||
@@ -9,29 +9,6 @@ const replyHandlers = new Map();
|
||||
const listeners = new Map();
|
||||
let messageQueue = [];
|
||||
let socketClient = null;
|
||||
let initPromise: Promise<T.ServerProxy> | null = null;
|
||||
|
||||
export const server: T.ServerProxy = new Proxy({} as T.ServerProxy, {
|
||||
get(_target, prop: string | symbol) {
|
||||
if (typeof prop === 'symbol') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Returning undefined for 'then' prevents the proxy from being
|
||||
// treated as a thenable when awaited, which would cause Promise
|
||||
// machinery to call server.then(resolve, reject) with native functions.
|
||||
if (prop === 'then') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!initPromise) {
|
||||
throw new Error(`Cannot use server proxy before init() has been called`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (args?: any) => send(prop as any, args);
|
||||
},
|
||||
});
|
||||
|
||||
function connectSocket(onOpen) {
|
||||
global.Actual.ipcConnect(function (client) {
|
||||
@@ -95,15 +72,12 @@ function connectSocket(onOpen) {
|
||||
messageQueue = [];
|
||||
}
|
||||
|
||||
onOpen(server);
|
||||
onOpen();
|
||||
});
|
||||
}
|
||||
|
||||
export const init: T.Init = function () {
|
||||
if (!initPromise) {
|
||||
initPromise = new Promise(connectSocket);
|
||||
}
|
||||
return initPromise;
|
||||
export const init: T.Init = async function () {
|
||||
return new Promise(connectSocket);
|
||||
};
|
||||
|
||||
export const send: T.Send = function (
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { App } from '../../../types/app';
|
||||
import type { Handlers } from '../../../types/handlers';
|
||||
import type { ServerEvents } from '../../../types/server-events';
|
||||
|
||||
export declare function init(
|
||||
channel: Window | number, // in electron the port number, in web the worker
|
||||
app: App<Handlers>,
|
||||
handlers: Handlers,
|
||||
): void;
|
||||
export type Init = typeof init;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { APIError } from '../../../server/errors';
|
||||
import { isMutating } from '../../../server/mutators';
|
||||
import { isMutating, runHandler } from '../../../server/mutators';
|
||||
import { captureException } from '../../exceptions';
|
||||
import { logger } from '../log';
|
||||
|
||||
@@ -14,12 +14,12 @@ function coerceError(error) {
|
||||
return { type: 'ServerError', message: error.message, cause: error };
|
||||
}
|
||||
|
||||
export const init: T.Init = function (_socketName, app) {
|
||||
export const init: T.Init = function (_socketName, handlers) {
|
||||
process.parentPort.on('message', ({ data }) => {
|
||||
const { id, name, args, undoTag, catchErrors } = data;
|
||||
|
||||
if (app.hasHandler(name)) {
|
||||
app.runHandler(name, args, { undoTag, name }).then(
|
||||
if (handlers[name]) {
|
||||
runHandler(handlers[name], args, { undoTag, name }).then(
|
||||
result => {
|
||||
if (catchErrors) {
|
||||
result = { data: result, error: null };
|
||||
@@ -30,9 +30,7 @@ export const init: T.Init = function (_socketName, app) {
|
||||
id,
|
||||
result,
|
||||
mutated:
|
||||
isMutating(app.getHandler(name)) &&
|
||||
name !== 'undo' &&
|
||||
name !== 'redo',
|
||||
isMutating(handlers[name]) && name !== 'undo' && name !== 'redo',
|
||||
undoTag,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { APIError } from '../../../server/errors';
|
||||
import { isMutating } from '../../../server/mutators';
|
||||
import { isMutating, runHandler } from '../../../server/mutators';
|
||||
import { captureException } from '../../exceptions';
|
||||
import { logger } from '../log';
|
||||
|
||||
@@ -31,7 +31,7 @@ function coerceError(error) {
|
||||
return { type: 'ServerError', message: error.message, cause: error };
|
||||
}
|
||||
|
||||
export const init: T.Init = function (serverChn, app) {
|
||||
export const init: T.Init = function (serverChn, handlers) {
|
||||
const serverChannel = serverChn as Window;
|
||||
getGlobalObject().__globalServerChannel = serverChannel;
|
||||
|
||||
@@ -54,14 +54,14 @@ export const init: T.Init = function (serverChn, app) {
|
||||
|
||||
const { id, name, args, undoTag, catchErrors } = msg;
|
||||
|
||||
if (app.hasHandler(name)) {
|
||||
app.runHandler(name, args, { undoTag, name }).then(
|
||||
if (handlers[name]) {
|
||||
runHandler(handlers[name], args, { undoTag, name }).then(
|
||||
result => {
|
||||
serverChannel.postMessage({
|
||||
type: 'reply',
|
||||
id,
|
||||
result: catchErrors ? { data: result, error: null } : result,
|
||||
mutated: isMutating(app.getHandler(name)),
|
||||
mutated: isMutating(handlers[name]),
|
||||
undoTag,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ vi.mock('./sync', async () => ({
|
||||
syncAccount: vi.fn(),
|
||||
}));
|
||||
|
||||
const simpleFinBatchSyncHandler = app['simplefin-batch-sync'];
|
||||
const simpleFinBatchSyncHandler = app.handlers['simplefin-batch-sync'];
|
||||
|
||||
function insertBank(bank: { id: string; bank_id: string; name: string }) {
|
||||
db.runQuery(
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
PostError,
|
||||
TransactionError,
|
||||
} from '../errors';
|
||||
import { mainApp } from '../main';
|
||||
import { app as mainApp } from '../main-app';
|
||||
import { mutator } from '../mutators';
|
||||
import { get, post } from '../post';
|
||||
import { getServer } from '../server-config';
|
||||
@@ -495,7 +495,7 @@ async function closeAccount({
|
||||
);
|
||||
}
|
||||
|
||||
await mainApp['transaction-add']({
|
||||
await mainApp.handlers['transaction-add']({
|
||||
id: uuidv4(),
|
||||
payee: transferPayee.id,
|
||||
amount: -balance,
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
// @ts-strict-ignore
|
||||
import { app as apiApp } from './api';
|
||||
import { mainApp } from './main';
|
||||
import { getBankSyncError } from '../shared/errors';
|
||||
import type { ServerHandlers } from '../types/server-handlers';
|
||||
|
||||
import { installAPI } from './api';
|
||||
vi.mock('../shared/errors', () => ({
|
||||
getBankSyncError: vi.fn(error => `Bank sync error: ${error}`),
|
||||
}));
|
||||
|
||||
describe('API handlers', () => {
|
||||
const handlers = installAPI({} as unknown as ServerHandlers);
|
||||
|
||||
describe('API app', () => {
|
||||
describe('api/bank-sync', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should sync a single account when accountId is provided', async () => {
|
||||
vi.spyOn(mainApp, 'runHandler').mockImplementation(
|
||||
async (name: string) => {
|
||||
if (name === 'accounts-bank-sync') return { errors: [] };
|
||||
throw new Error(`Unexpected handler: ${name}`);
|
||||
},
|
||||
);
|
||||
handlers['accounts-bank-sync'] = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ errors: [] });
|
||||
|
||||
await apiApp['api/bank-sync']({ accountId: 'account1' });
|
||||
expect(mainApp.runHandler.bind(mainApp)).toHaveBeenCalledWith(
|
||||
'accounts-bank-sync',
|
||||
{
|
||||
ids: ['account1'],
|
||||
},
|
||||
);
|
||||
await handlers['api/bank-sync']({ accountId: 'account1' });
|
||||
expect(handlers['accounts-bank-sync']).toHaveBeenCalledWith({
|
||||
ids: ['account1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when bank sync fails', async () => {
|
||||
vi.spyOn(mainApp, 'runHandler').mockImplementation(
|
||||
async (name: string) => {
|
||||
if (name === 'accounts-bank-sync') {
|
||||
return { errors: [{ message: 'connection-failed' }] };
|
||||
}
|
||||
throw new Error(`Unexpected handler: ${name}`);
|
||||
},
|
||||
);
|
||||
it('should handle errors in non batch sync', async () => {
|
||||
handlers['accounts-bank-sync'] = vi.fn().mockResolvedValue({
|
||||
errors: ['connection-failed'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
apiApp['api/bank-sync']({ accountId: 'account2' }),
|
||||
).rejects.toThrow('connection-failed');
|
||||
handlers['api/bank-sync']({ accountId: 'account2' }),
|
||||
).rejects.toThrow('Bank sync error: connection-failed');
|
||||
|
||||
expect(getBankSyncError).toHaveBeenCalledWith('connection-failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,12 +17,13 @@ import {
|
||||
updateTransaction,
|
||||
} from '../shared/transactions';
|
||||
import { integerToAmount } from '../shared/util';
|
||||
import type { ApiHandlers } from '../types/api-handlers';
|
||||
import type { Handlers } from '../types/handlers';
|
||||
import type {
|
||||
AccountEntity,
|
||||
CategoryGroupEntity,
|
||||
ScheduleEntity,
|
||||
} from '../types/models';
|
||||
import type { ServerHandlers } from '../types/server-handlers';
|
||||
|
||||
import { addTransactions } from './accounts/sync';
|
||||
import {
|
||||
@@ -36,13 +37,11 @@ import {
|
||||
tagModel,
|
||||
} from './api-models';
|
||||
import type { AmountOPType, APIScheduleEntity } from './api-models';
|
||||
import { createApp } from './app';
|
||||
import { aqlQuery } from './aql';
|
||||
import * as cloudStorage from './cloud-storage';
|
||||
import type { RemoteFile } from './cloud-storage';
|
||||
import * as db from './db';
|
||||
import { APIError } from './errors';
|
||||
import { mainApp } from './main';
|
||||
import { runMutator } from './mutators';
|
||||
import * as prefs from './prefs';
|
||||
import * as sheet from './sheet';
|
||||
@@ -83,7 +82,7 @@ function withMutation<Params extends Array<unknown>, ReturnType>(
|
||||
};
|
||||
}
|
||||
|
||||
const handlers = {} as ApiHandlers;
|
||||
let handlers = {} as unknown as Handlers;
|
||||
|
||||
async function validateMonth(month) {
|
||||
if (!month.match(/^\d{4}-\d{2}$/)) {
|
||||
@@ -91,7 +90,7 @@ async function validateMonth(month) {
|
||||
}
|
||||
|
||||
if (!IMPORT_MODE) {
|
||||
const { start, end } = await mainApp['get-budget-bounds']();
|
||||
const { start, end } = await handlers['get-budget-bounds']();
|
||||
const range = monthUtils.range(start, end);
|
||||
if (!range.includes(month)) {
|
||||
throw APIError('No budget exists for month: ' + month);
|
||||
@@ -163,7 +162,7 @@ handlers['api/load-budget'] = async function ({ id }) {
|
||||
|
||||
if (currentId !== id) {
|
||||
connection.send('start-load');
|
||||
const { error } = await mainApp['load-budget']({ id });
|
||||
const { error } = await handlers['load-budget']({ id });
|
||||
|
||||
if (!error) {
|
||||
connection.send('finish-load');
|
||||
@@ -178,16 +177,16 @@ handlers['api/load-budget'] = async function ({ id }) {
|
||||
handlers['api/download-budget'] = async function ({ syncId, password }) {
|
||||
const { id: currentId } = prefs.getPrefs() || {};
|
||||
if (currentId) {
|
||||
await mainApp['close-budget']();
|
||||
await handlers['close-budget']();
|
||||
}
|
||||
|
||||
const budgets = await mainApp['get-budgets']();
|
||||
const budgets = await handlers['get-budgets']();
|
||||
const localBudget = budgets.find(b => b.groupId === syncId);
|
||||
let remoteBudget: RemoteFile;
|
||||
|
||||
// Load a remote file if we could not find the file locally
|
||||
if (!localBudget) {
|
||||
const files = await mainApp['get-remote-files']();
|
||||
const files = await handlers['get-remote-files']();
|
||||
if (!files) {
|
||||
throw new Error('Could not get remote files');
|
||||
}
|
||||
@@ -211,7 +210,7 @@ handlers['api/download-budget'] = async function ({ syncId, password }) {
|
||||
);
|
||||
}
|
||||
|
||||
const result = await mainApp['key-test']({
|
||||
const result = await handlers['key-test']({
|
||||
cloudFileId: remoteBudget ? remoteBudget.fileId : localBudget.cloudFileId,
|
||||
password,
|
||||
});
|
||||
@@ -222,8 +221,8 @@ handlers['api/download-budget'] = async function ({ syncId, password }) {
|
||||
|
||||
// Sync the local budget file
|
||||
if (localBudget) {
|
||||
await mainApp['load-budget']({ id: localBudget.id });
|
||||
const result = await mainApp['sync-budget']();
|
||||
await handlers['load-budget']({ id: localBudget.id });
|
||||
const result = await handlers['sync-budget']();
|
||||
if (result.error) {
|
||||
throw new Error(getSyncError(result.error.reason, localBudget.id));
|
||||
}
|
||||
@@ -231,19 +230,19 @@ handlers['api/download-budget'] = async function ({ syncId, password }) {
|
||||
}
|
||||
|
||||
// Download the remote file (no need to perform a sync as the file will already be up-to-date)
|
||||
const result = await mainApp['download-budget']({
|
||||
const result = await handlers['download-budget']({
|
||||
cloudFileId: remoteBudget.fileId,
|
||||
});
|
||||
if (result.error) {
|
||||
logger.log('Full error details', result.error);
|
||||
throw new Error(getDownloadError(result.error));
|
||||
}
|
||||
await mainApp['load-budget']({ id: result.id });
|
||||
await handlers['load-budget']({ id: result.id });
|
||||
};
|
||||
|
||||
handlers['api/get-budgets'] = async function () {
|
||||
const budgets = await mainApp['get-budgets']();
|
||||
const files = (await mainApp['get-remote-files']()) || [];
|
||||
const budgets = await handlers['get-budgets']();
|
||||
const files = (await handlers['get-remote-files']()) || [];
|
||||
return [
|
||||
...budgets.map(file => budgetModel.toExternal(file)),
|
||||
...files.map(file => remoteFileModel.toExternal(file)).filter(file => file),
|
||||
@@ -252,7 +251,7 @@ handlers['api/get-budgets'] = async function () {
|
||||
|
||||
handlers['api/sync'] = async function () {
|
||||
const { id } = prefs.getPrefs();
|
||||
const result = await mainApp['sync-budget']();
|
||||
const result = await handlers['sync-budget']();
|
||||
if (result.error) {
|
||||
throw new Error(getSyncError(result.error.reason, id));
|
||||
}
|
||||
@@ -263,13 +262,13 @@ handlers['api/bank-sync'] = async function (args) {
|
||||
const allErrors = [];
|
||||
|
||||
if (!batchSync) {
|
||||
const { errors } = await mainApp['accounts-bank-sync']({
|
||||
const { errors } = await handlers['accounts-bank-sync']({
|
||||
ids: [args.accountId],
|
||||
});
|
||||
|
||||
allErrors.push(...errors);
|
||||
} else {
|
||||
const accountsData = await mainApp['accounts-get']();
|
||||
const accountsData = await handlers['accounts-get']();
|
||||
const accountIdsToSync = accountsData.map(a => a.id);
|
||||
const simpleFinAccounts = accountsData.filter(
|
||||
a => a.account_sync_source === 'simpleFin',
|
||||
@@ -277,14 +276,14 @@ handlers['api/bank-sync'] = async function (args) {
|
||||
const simpleFinAccountIds = simpleFinAccounts.map(a => a.id);
|
||||
|
||||
if (simpleFinAccounts.length > 1) {
|
||||
const res = await mainApp['simplefin-batch-sync']({
|
||||
const res = await handlers['simplefin-batch-sync']({
|
||||
ids: simpleFinAccountIds,
|
||||
});
|
||||
|
||||
res.forEach(a => allErrors.push(...a.res.errors));
|
||||
}
|
||||
|
||||
const { errors } = await mainApp['accounts-bank-sync']({
|
||||
const { errors } = await handlers['accounts-bank-sync']({
|
||||
ids: accountIdsToSync.filter(a => !simpleFinAccountIds.includes(a)),
|
||||
});
|
||||
|
||||
@@ -299,10 +298,10 @@ handlers['api/bank-sync'] = async function (args) {
|
||||
|
||||
handlers['api/start-import'] = async function ({ budgetName }) {
|
||||
// Notify UI to close budget
|
||||
await mainApp['close-budget']();
|
||||
await handlers['close-budget']();
|
||||
|
||||
// Create the budget
|
||||
await mainApp['create-budget']({ budgetName, avoidUpload: true });
|
||||
await handlers['create-budget']({ budgetName, avoidUpload: true });
|
||||
|
||||
// Clear out the default expense categories
|
||||
db.runQuery('DELETE FROM categories WHERE is_income = 0');
|
||||
@@ -324,10 +323,10 @@ handlers['api/finish-import'] = async function () {
|
||||
// the spreadsheet, but we can't just recreate the spreadsheet
|
||||
// either; there is other internal state that isn't created
|
||||
const { id } = prefs.getPrefs();
|
||||
await mainApp['close-budget']();
|
||||
await mainApp['load-budget']({ id });
|
||||
await handlers['close-budget']();
|
||||
await handlers['load-budget']({ id });
|
||||
|
||||
await mainApp['get-budget-bounds']();
|
||||
await handlers['get-budget-bounds']();
|
||||
await sheet.waitOnSpreadsheet();
|
||||
|
||||
await cloudStorage.upload().catch(() => {
|
||||
@@ -344,8 +343,8 @@ handlers['api/abort-import'] = async function () {
|
||||
|
||||
const { id } = prefs.getPrefs();
|
||||
|
||||
await mainApp['close-budget']();
|
||||
await mainApp['delete-budget']({ id });
|
||||
await handlers['close-budget']();
|
||||
await handlers['delete-budget']({ id });
|
||||
connection.send('show-budgets');
|
||||
}
|
||||
|
||||
@@ -359,7 +358,7 @@ handlers['api/query'] = async function ({ query }) {
|
||||
|
||||
handlers['api/budget-months'] = async function () {
|
||||
checkFileOpen();
|
||||
const { start, end } = await mainApp['get-budget-bounds']();
|
||||
const { start, end } = await handlers['get-budget-bounds']();
|
||||
return monthUtils.range(start, end);
|
||||
};
|
||||
|
||||
@@ -429,7 +428,7 @@ handlers['api/budget-set-amount'] = withMutation(async function ({
|
||||
amount,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['budget/budget-amount']({
|
||||
return handlers['budget/budget-amount']({
|
||||
month,
|
||||
category: categoryId,
|
||||
amount,
|
||||
@@ -444,7 +443,7 @@ handlers['api/budget-set-carryover'] = withMutation(async function ({
|
||||
checkFileOpen();
|
||||
await validateMonth(month);
|
||||
await validateExpenseCategory('budget-set-carryover', categoryId);
|
||||
return mainApp['budget/set-carryover']({
|
||||
return handlers['budget/set-carryover']({
|
||||
startMonth: month,
|
||||
category: categoryId,
|
||||
flag,
|
||||
@@ -460,7 +459,7 @@ handlers['api/budget-hold-for-next-month'] = withMutation(async function ({
|
||||
if (amount <= 0) {
|
||||
throw APIError('Amount to hold needs to be greater than 0');
|
||||
}
|
||||
return mainApp['budget/hold-for-next-month']({
|
||||
return handlers['budget/hold-for-next-month']({
|
||||
month,
|
||||
amount,
|
||||
});
|
||||
@@ -469,7 +468,7 @@ handlers['api/budget-hold-for-next-month'] = withMutation(async function ({
|
||||
handlers['api/budget-reset-hold'] = withMutation(async function ({ month }) {
|
||||
checkFileOpen();
|
||||
await validateMonth(month);
|
||||
return mainApp['budget/reset-hold']({ month });
|
||||
return handlers['budget/reset-hold']({ month });
|
||||
});
|
||||
|
||||
handlers['api/transactions-export'] = async function ({
|
||||
@@ -479,7 +478,7 @@ handlers['api/transactions-export'] = async function ({
|
||||
accounts,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['transactions-export']({
|
||||
return handlers['transactions-export']({
|
||||
transactions,
|
||||
categoryGroups,
|
||||
payees,
|
||||
@@ -494,7 +493,7 @@ handlers['api/transactions-import'] = withMutation(async function ({
|
||||
opts,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['transactions-import']({
|
||||
return handlers['transactions-import']({
|
||||
accountId,
|
||||
transactions,
|
||||
isPreview,
|
||||
@@ -552,7 +551,7 @@ handlers['api/transaction-update'] = withMutation(async function ({
|
||||
}
|
||||
|
||||
const { diff } = updateTransaction(transactions, { id, ...fields });
|
||||
return mainApp['transactions-batch-update'](diff)['updated'];
|
||||
return handlers['transactions-batch-update'](diff)['updated'];
|
||||
});
|
||||
|
||||
handlers['api/transaction-delete'] = withMutation(async function ({ id }) {
|
||||
@@ -567,12 +566,12 @@ handlers['api/transaction-delete'] = withMutation(async function ({ id }) {
|
||||
}
|
||||
|
||||
const { diff } = deleteTransaction(transactions, id);
|
||||
return mainApp['transactions-batch-update'](diff)['deleted'];
|
||||
return handlers['transactions-batch-update'](diff)['deleted'];
|
||||
});
|
||||
|
||||
handlers['api/accounts-get'] = async function () {
|
||||
checkFileOpen();
|
||||
const accounts: AccountEntity[] = await mainApp['accounts-get']();
|
||||
const accounts: AccountEntity[] = await handlers['accounts-get']();
|
||||
return accounts.map(account => accountModel.toExternal(account));
|
||||
};
|
||||
|
||||
@@ -581,7 +580,7 @@ handlers['api/account-create'] = withMutation(async function ({
|
||||
initialBalance = null,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['account-create']({
|
||||
return handlers['account-create']({
|
||||
name: account.name,
|
||||
offBudget: account.offbudget,
|
||||
closed: account.closed,
|
||||
@@ -593,7 +592,7 @@ handlers['api/account-create'] = withMutation(async function ({
|
||||
|
||||
handlers['api/account-update'] = withMutation(async function ({ id, fields }) {
|
||||
checkFileOpen();
|
||||
await mainApp['account-update']({ id, ...accountModel.fromExternal(fields) });
|
||||
return db.updateAccount({ id, ...accountModel.fromExternal(fields) });
|
||||
});
|
||||
|
||||
handlers['api/account-close'] = withMutation(async function ({
|
||||
@@ -602,7 +601,7 @@ handlers['api/account-close'] = withMutation(async function ({
|
||||
transferCategoryId,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['account-close']({
|
||||
return handlers['account-close']({
|
||||
id,
|
||||
transferAccountId,
|
||||
categoryId: transferCategoryId,
|
||||
@@ -611,12 +610,12 @@ handlers['api/account-close'] = withMutation(async function ({
|
||||
|
||||
handlers['api/account-reopen'] = withMutation(async function ({ id }) {
|
||||
checkFileOpen();
|
||||
return mainApp['account-reopen']({ id });
|
||||
return handlers['account-reopen']({ id });
|
||||
});
|
||||
|
||||
handlers['api/account-delete'] = withMutation(async function ({ id }) {
|
||||
checkFileOpen();
|
||||
return mainApp['account-close']({ id, forced: true });
|
||||
return handlers['account-close']({ id, forced: true });
|
||||
});
|
||||
|
||||
handlers['api/account-balance'] = withMutation(async function ({
|
||||
@@ -624,14 +623,14 @@ handlers['api/account-balance'] = withMutation(async function ({
|
||||
cutoff = new Date(),
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['account-balance']({ id, cutoff });
|
||||
return handlers['account-balance']({ id, cutoff });
|
||||
});
|
||||
|
||||
handlers['api/categories-get'] = async function ({
|
||||
grouped,
|
||||
}: { grouped? } = {}) {
|
||||
checkFileOpen();
|
||||
const result = await mainApp['get-categories']();
|
||||
const result = await handlers['get-categories']();
|
||||
return grouped
|
||||
? result.grouped.map(group => categoryGroupModel.toExternal(group))
|
||||
: result.list.map(category => categoryModel.toExternal(category));
|
||||
@@ -639,7 +638,7 @@ handlers['api/categories-get'] = async function ({
|
||||
|
||||
handlers['api/category-groups-get'] = async function () {
|
||||
checkFileOpen();
|
||||
const groups = await mainApp['get-category-groups']();
|
||||
const groups = await handlers['get-category-groups']();
|
||||
return groups.map(group => categoryGroupModel.toExternal(group));
|
||||
};
|
||||
|
||||
@@ -647,7 +646,7 @@ handlers['api/category-group-create'] = withMutation(async function ({
|
||||
group,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['category-group-create']({
|
||||
return handlers['category-group-create']({
|
||||
name: group.name,
|
||||
hidden: group.hidden,
|
||||
});
|
||||
@@ -658,7 +657,7 @@ handlers['api/category-group-update'] = withMutation(async function ({
|
||||
fields,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['category-group-update']({
|
||||
return handlers['category-group-update']({
|
||||
id,
|
||||
...categoryGroupModel.fromExternal(fields),
|
||||
});
|
||||
@@ -669,7 +668,7 @@ handlers['api/category-group-delete'] = withMutation(async function ({
|
||||
transferCategoryId,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['category-group-delete']({
|
||||
return handlers['category-group-delete']({
|
||||
id,
|
||||
transferId: transferCategoryId,
|
||||
});
|
||||
@@ -677,7 +676,7 @@ handlers['api/category-group-delete'] = withMutation(async function ({
|
||||
|
||||
handlers['api/category-create'] = withMutation(async function ({ category }) {
|
||||
checkFileOpen();
|
||||
return mainApp['category-create']({
|
||||
return handlers['category-create']({
|
||||
name: category.name,
|
||||
groupId: category.group_id,
|
||||
isIncome: category.is_income,
|
||||
@@ -687,7 +686,7 @@ handlers['api/category-create'] = withMutation(async function ({ category }) {
|
||||
|
||||
handlers['api/category-update'] = withMutation(async function ({ id, fields }) {
|
||||
checkFileOpen();
|
||||
return mainApp['category-update']({
|
||||
return handlers['category-update']({
|
||||
id,
|
||||
...categoryModel.fromExternal(fields),
|
||||
});
|
||||
@@ -698,7 +697,7 @@ handlers['api/category-delete'] = withMutation(async function ({
|
||||
transferCategoryId,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp['category-delete']({
|
||||
return handlers['category-delete']({
|
||||
id,
|
||||
transferId: transferCategoryId,
|
||||
});
|
||||
@@ -706,31 +705,31 @@ handlers['api/category-delete'] = withMutation(async function ({
|
||||
|
||||
handlers['api/common-payees-get'] = async function () {
|
||||
checkFileOpen();
|
||||
const payees = await mainApp.getCommonPayees();
|
||||
const payees = await handlers['common-payees-get']();
|
||||
return payees.map(payee => payeeModel.toExternal(payee));
|
||||
};
|
||||
|
||||
handlers['api/payees-get'] = async function () {
|
||||
checkFileOpen();
|
||||
const payees = await mainApp.getPayees();
|
||||
const payees = await handlers['payees-get']();
|
||||
return payees.map(payee => payeeModel.toExternal(payee));
|
||||
};
|
||||
|
||||
handlers['api/payee-create'] = withMutation(async function ({ payee }) {
|
||||
checkFileOpen();
|
||||
return mainApp.createPayee({ name: payee.name });
|
||||
return handlers['payee-create']({ name: payee.name });
|
||||
});
|
||||
|
||||
handlers['api/payee-update'] = withMutation(async function ({ id, fields }) {
|
||||
checkFileOpen();
|
||||
return mainApp.batchChangePayees({
|
||||
return handlers['payees-batch-change']({
|
||||
updated: [{ id, ...payeeModel.fromExternal(fields) }],
|
||||
});
|
||||
});
|
||||
|
||||
handlers['api/payee-delete'] = withMutation(async function ({ id }) {
|
||||
checkFileOpen();
|
||||
return mainApp.batchChangePayees({ deleted: [{ id }] });
|
||||
return handlers['payees-batch-change']({ deleted: [{ id }] });
|
||||
});
|
||||
|
||||
handlers['api/payees-merge'] = withMutation(async function ({
|
||||
@@ -738,18 +737,18 @@ handlers['api/payees-merge'] = withMutation(async function ({
|
||||
mergeIds,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp.mergePayees({ targetId, mergeIds });
|
||||
return handlers['payees-merge']({ targetId, mergeIds });
|
||||
});
|
||||
|
||||
handlers['api/tags-get'] = async function () {
|
||||
checkFileOpen();
|
||||
const tags = await mainApp['tags-get']();
|
||||
const tags = await handlers['tags-get']();
|
||||
return tags.map(tag => tagModel.toExternal(tag));
|
||||
};
|
||||
|
||||
handlers['api/tag-create'] = withMutation(async function ({ tag }) {
|
||||
checkFileOpen();
|
||||
const result = await mainApp['tags-create']({
|
||||
const result = await handlers['tags-create']({
|
||||
tag: tag.tag,
|
||||
color: tag.color,
|
||||
description: tag.description,
|
||||
@@ -759,12 +758,12 @@ handlers['api/tag-create'] = withMutation(async function ({ tag }) {
|
||||
|
||||
handlers['api/tag-update'] = withMutation(async function ({ id, fields }) {
|
||||
checkFileOpen();
|
||||
await mainApp['tags-update']({ id, ...tagModel.fromExternal(fields) });
|
||||
await handlers['tags-update']({ id, ...tagModel.fromExternal(fields) });
|
||||
});
|
||||
|
||||
handlers['api/tag-delete'] = withMutation(async function ({ id }) {
|
||||
checkFileOpen();
|
||||
await mainApp['tags-delete']({ id });
|
||||
await handlers['tags-delete']({ id });
|
||||
});
|
||||
|
||||
handlers['api/payee-location-create'] = withMutation(async function ({
|
||||
@@ -773,17 +772,17 @@ handlers['api/payee-location-create'] = withMutation(async function ({
|
||||
longitude,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp.createPayeeLocation({ payeeId, latitude, longitude });
|
||||
return handlers['payee-location-create']({ payeeId, latitude, longitude });
|
||||
});
|
||||
|
||||
handlers['api/payee-locations-get'] = async function ({ payeeId }) {
|
||||
checkFileOpen();
|
||||
return mainApp.getPayeeLocations({ payeeId });
|
||||
return handlers['payee-locations-get']({ payeeId });
|
||||
};
|
||||
|
||||
handlers['api/payee-location-delete'] = withMutation(async function ({ id }) {
|
||||
checkFileOpen();
|
||||
return mainApp.deletePayeeLocation({ id });
|
||||
return handlers['payee-location-delete']({ id });
|
||||
});
|
||||
|
||||
handlers['api/payees-get-nearby'] = async function ({
|
||||
@@ -792,22 +791,22 @@ handlers['api/payees-get-nearby'] = async function ({
|
||||
maxDistance,
|
||||
}) {
|
||||
checkFileOpen();
|
||||
return mainApp.getNearbyPayees({ latitude, longitude, maxDistance });
|
||||
return handlers['payees-get-nearby']({ latitude, longitude, maxDistance });
|
||||
};
|
||||
|
||||
handlers['api/rules-get'] = async function () {
|
||||
checkFileOpen();
|
||||
return mainApp['rules-get']();
|
||||
return handlers['rules-get']();
|
||||
};
|
||||
|
||||
handlers['api/payee-rules-get'] = async function ({ id }) {
|
||||
checkFileOpen();
|
||||
return mainApp.getPayeeRules({ id });
|
||||
return handlers['payees-get-rules']({ id });
|
||||
};
|
||||
|
||||
handlers['api/rule-create'] = withMutation(async function ({ rule }) {
|
||||
checkFileOpen();
|
||||
const addedRule = await mainApp['rule-add'](rule);
|
||||
const addedRule = await handlers['rule-add'](rule);
|
||||
|
||||
if ('error' in addedRule) {
|
||||
throw APIError('Failed creating a new rule', addedRule.error);
|
||||
@@ -818,7 +817,7 @@ handlers['api/rule-create'] = withMutation(async function ({ rule }) {
|
||||
|
||||
handlers['api/rule-update'] = withMutation(async function ({ rule }) {
|
||||
checkFileOpen();
|
||||
const updatedRule = await mainApp['rule-update'](rule);
|
||||
const updatedRule = await handlers['rule-update'](rule);
|
||||
|
||||
if ('error' in updatedRule) {
|
||||
throw APIError('Failed updating the rule', updatedRule.error);
|
||||
@@ -829,7 +828,7 @@ handlers['api/rule-update'] = withMutation(async function ({ rule }) {
|
||||
|
||||
handlers['api/rule-delete'] = withMutation(async function (id) {
|
||||
checkFileOpen();
|
||||
return mainApp['rule-delete'](id);
|
||||
return handlers['rule-delete'](id);
|
||||
});
|
||||
|
||||
handlers['api/schedules-get'] = async function () {
|
||||
@@ -848,7 +847,7 @@ handlers['api/schedule-create'] = withMutation(async function (
|
||||
name: internalSchedule.name,
|
||||
posts_transaction: internalSchedule.posts_transaction,
|
||||
};
|
||||
return mainApp['schedule/create']({
|
||||
return handlers['schedule/create']({
|
||||
schedule: partialSchedule,
|
||||
conditions: internalSchedule._conditions,
|
||||
});
|
||||
@@ -982,7 +981,7 @@ handlers['api/schedule-update'] = withMutation(async function ({
|
||||
}
|
||||
|
||||
if (conditionsUpdated) {
|
||||
return mainApp['schedule/update']({
|
||||
return handlers['schedule/update']({
|
||||
schedule: {
|
||||
id: sched.id,
|
||||
posts_transaction: sched.posts_transaction,
|
||||
@@ -998,7 +997,7 @@ handlers['api/schedule-update'] = withMutation(async function ({
|
||||
|
||||
handlers['api/schedule-delete'] = withMutation(async function (id: string) {
|
||||
checkFileOpen();
|
||||
return mainApp['schedule/delete']({ id });
|
||||
return handlers['schedule/delete']({ id });
|
||||
});
|
||||
|
||||
handlers['api/get-id-by-name'] = async function ({ type, name }) {
|
||||
@@ -1021,7 +1020,11 @@ handlers['api/get-id-by-name'] = async function ({ type, name }) {
|
||||
|
||||
handlers['api/get-server-version'] = async function () {
|
||||
checkFileOpen();
|
||||
return mainApp['get-server-version']();
|
||||
return handlers['get-server-version']();
|
||||
};
|
||||
|
||||
export const app = createApp(handlers);
|
||||
export function installAPI(serverHandlers: ServerHandlers) {
|
||||
const merged = Object.assign({}, serverHandlers, handlers);
|
||||
handlers = merged as Handlers;
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import type { Emitter } from 'mitt';
|
||||
import { captureException } from '../platform/exceptions';
|
||||
import type { ServerEvents } from '../types/server-events';
|
||||
|
||||
import { runHandler as mutatorRunHandler } from './mutators';
|
||||
|
||||
// This is a simple helper abstraction for defining methods exposed to
|
||||
// the client. It doesn't do much, but checks for naming conflicts and
|
||||
// makes it cleaner to combine methods. We call a group of related
|
||||
@@ -20,32 +18,22 @@ type Events = {
|
||||
type UnlistenService = () => void;
|
||||
type Service = () => UnlistenService;
|
||||
|
||||
export class App<THandlers> {
|
||||
private handlers: THandlers;
|
||||
private services: Service[];
|
||||
private unlistenServices: UnlistenService[];
|
||||
class App<Handlers> {
|
||||
events: Emitter<Events>;
|
||||
handlers: Handlers;
|
||||
services: Service[];
|
||||
unlistenServices: UnlistenService[];
|
||||
|
||||
readonly events: Emitter<Events>;
|
||||
|
||||
constructor(handlers?: THandlers) {
|
||||
this.handlers = {} as THandlers;
|
||||
constructor() {
|
||||
this.handlers = {} as Handlers;
|
||||
this.services = [];
|
||||
this.events = mitt<Events>();
|
||||
this.unlistenServices = [];
|
||||
|
||||
if (handlers) {
|
||||
for (const [name, func] of Object.entries(handlers)) {
|
||||
this.method(
|
||||
name as string & keyof THandlers,
|
||||
func as THandlers[string & keyof THandlers],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
method<Name extends string & keyof THandlers>(
|
||||
method<Name extends string & keyof Handlers>(
|
||||
name: Name,
|
||||
func: THandlers[Name],
|
||||
func: Handlers[Name],
|
||||
) {
|
||||
if (this.handlers[name] != null) {
|
||||
throw new Error(
|
||||
@@ -62,7 +50,7 @@ export class App<THandlers> {
|
||||
combine(...apps) {
|
||||
for (const app of apps) {
|
||||
Object.keys(app.handlers).forEach(name => {
|
||||
this.method(name as string & keyof THandlers, app.handlers[name]);
|
||||
this.method(name as string & keyof Handlers, app.handlers[name]);
|
||||
});
|
||||
|
||||
app.services.forEach(service => {
|
||||
@@ -96,50 +84,8 @@ export class App<THandlers> {
|
||||
});
|
||||
this.unlistenServices = [];
|
||||
}
|
||||
|
||||
getHandler<T extends keyof THandlers>(name: T): THandlers[T] {
|
||||
return this.handlers[name];
|
||||
}
|
||||
|
||||
hasHandler<T extends keyof THandlers>(name: T): boolean {
|
||||
return this.getHandler(name) != null;
|
||||
}
|
||||
|
||||
async runHandler<T extends keyof THandlers>(
|
||||
name: T,
|
||||
args?: THandlers[T] extends (...a: infer A) => unknown ? A[0] : never,
|
||||
options?: Parameters<typeof mutatorRunHandler>[2],
|
||||
): Promise<
|
||||
THandlers[T] extends (...a: infer _A) => Promise<infer R> ? R : never
|
||||
> {
|
||||
const handler = this.handlers[name];
|
||||
if (!handler) {
|
||||
throw new Error(`No handler for method: ${String(name)}`);
|
||||
}
|
||||
return mutatorRunHandler(
|
||||
handler as Parameters<typeof mutatorRunHandler>[0],
|
||||
args,
|
||||
options,
|
||||
) as Promise<
|
||||
THandlers[T] extends (...a: infer _A) => Promise<infer R> ? R : never
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp<THandlers>(
|
||||
handlers?: THandlers,
|
||||
): App<THandlers> & THandlers {
|
||||
const app = new App<THandlers>(handlers);
|
||||
return new Proxy(app, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop in target) {
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
return new Proxy(target.runHandler.bind(target), {
|
||||
apply(boundFn, _thisArg, [args]) {
|
||||
return boundFn(prop as keyof THandlers, args);
|
||||
},
|
||||
});
|
||||
},
|
||||
}) as App<THandlers> & THandlers;
|
||||
export function createApp<T>() {
|
||||
return new App<T>();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as db from '../db';
|
||||
import * as mappings from '../db/mappings';
|
||||
import { handleBudgetImport } from '../importers';
|
||||
import type { ImportableBudgetType } from '../importers';
|
||||
import { mainApp } from '../main';
|
||||
import { app as mainApp } from '../main-app';
|
||||
import { mutator } from '../mutators';
|
||||
import * as prefs from '../prefs';
|
||||
import { getServer } from '../server-config';
|
||||
@@ -453,7 +453,7 @@ async function createBudget({
|
||||
}
|
||||
|
||||
if (testMode) {
|
||||
await createTestBudget(mainApp);
|
||||
await createTestBudget(mainApp.handlers);
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import * as fs from '../../platform/server/fs';
|
||||
import * as sqlite from '../../platform/server/sqlite';
|
||||
import * as cloudStorage from '../cloud-storage';
|
||||
import { mainApp } from '../main';
|
||||
import { handlers } from '../main';
|
||||
import { waitOnSpreadsheet } from '../sheet';
|
||||
|
||||
export async function importActual(_filepath: string, buffer: Buffer) {
|
||||
// Importing Actual files is a special case because we can directly
|
||||
// write down the files, but because it doesn't go through the API
|
||||
// layer we need to duplicate some of the workflow
|
||||
await mainApp['close-budget']();
|
||||
await handlers['close-budget']();
|
||||
|
||||
let id;
|
||||
try {
|
||||
@@ -40,8 +40,8 @@ export async function importActual(_filepath: string, buffer: Buffer) {
|
||||
|
||||
// Load the budget, force everything to be computed, and try
|
||||
// to upload it as a cloud file
|
||||
await mainApp['load-budget']({ id });
|
||||
await mainApp['get-budget-bounds']();
|
||||
await handlers['load-budget']({ id });
|
||||
await handlers['get-budget-bounds']();
|
||||
await waitOnSpreadsheet();
|
||||
await cloudStorage.upload().catch(() => {
|
||||
// Ignore errors
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { logger } from '../../platform/server/log';
|
||||
import { mainApp } from '../main';
|
||||
import { handlers } from '../main';
|
||||
|
||||
import { importActual } from './actual';
|
||||
import * as YNAB4 from './ynab4';
|
||||
@@ -42,17 +42,17 @@ export async function handleBudgetImport(
|
||||
}
|
||||
|
||||
try {
|
||||
await mainApp['api/start-import']({ budgetName });
|
||||
await handlers['api/start-import']({ budgetName });
|
||||
} catch (e) {
|
||||
logger.error('failed to start import', e);
|
||||
return { error: 'unknown' };
|
||||
}
|
||||
await importer.doImport(data);
|
||||
} catch (e) {
|
||||
await mainApp['api/abort-import']();
|
||||
await handlers['api/abort-import']();
|
||||
logger.error('failed to run import', e);
|
||||
return { error: 'unknown' };
|
||||
}
|
||||
|
||||
await mainApp['api/finish-import']();
|
||||
await handlers['api/finish-import']();
|
||||
}
|
||||
|
||||
@@ -6,7 +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 { mainApp } from '../main';
|
||||
import { send } from '../main-app';
|
||||
|
||||
import type * as YNAB4 from './ynab4-types';
|
||||
|
||||
@@ -21,7 +21,7 @@ async function importAccounts(
|
||||
return Promise.all(
|
||||
accounts.map(async account => {
|
||||
if (!account.isTombstone) {
|
||||
const id = await mainApp['api/account-create']({
|
||||
const id = await send('api/account-create', {
|
||||
account: {
|
||||
name: account.accountName,
|
||||
offbudget: account.onBudget ? false : true,
|
||||
@@ -48,7 +48,7 @@ async function importCategories(
|
||||
masterCategory.subCategories &&
|
||||
masterCategory.subCategories.some(cat => !cat.isTombstone)
|
||||
) {
|
||||
const id = await mainApp['api/category-group-create']({
|
||||
const id = await send('api/category-group-create', {
|
||||
group: {
|
||||
name: masterCategory.name,
|
||||
is_income: false,
|
||||
@@ -56,7 +56,7 @@ async function importCategories(
|
||||
});
|
||||
entityIdMap.set(masterCategory.entityId, id);
|
||||
if (masterCategory.note) {
|
||||
void mainApp['notes-save']({
|
||||
void send('notes-save', {
|
||||
id,
|
||||
note: masterCategory.note,
|
||||
});
|
||||
@@ -89,7 +89,7 @@ async function importCategories(
|
||||
categoryName = categoryNameParts.join('/').trim();
|
||||
}
|
||||
|
||||
const id = await mainApp['api/category-create']({
|
||||
const id = await send('api/category-create', {
|
||||
category: {
|
||||
name: categoryName,
|
||||
group_id: entityIdMap.get(category.masterCategoryId),
|
||||
@@ -97,7 +97,7 @@ async function importCategories(
|
||||
});
|
||||
entityIdMap.set(category.entityId, id);
|
||||
if (category.note) {
|
||||
void mainApp['notes-save']({
|
||||
void send('notes-save', {
|
||||
id,
|
||||
note: category.note,
|
||||
});
|
||||
@@ -116,7 +116,7 @@ async function importPayees(
|
||||
) {
|
||||
for (const payee of data.payees) {
|
||||
if (!payee.isTombstone) {
|
||||
const id = await mainApp['api/payee-create']({
|
||||
const id = await send('api/payee-create', {
|
||||
payee: {
|
||||
name: payee.name,
|
||||
transfer_acct: entityIdMap.get(payee.targetAccountId) || null,
|
||||
@@ -134,14 +134,14 @@ async function importTransactions(
|
||||
data: YNAB4.YFull,
|
||||
entityIdMap: Map<string, string>,
|
||||
) {
|
||||
const categories = await mainApp['api/categories-get']({
|
||||
const categories = await send('api/categories-get', {
|
||||
grouped: false,
|
||||
});
|
||||
const incomeCategoryId: string = categories.find(
|
||||
cat => cat.name === 'Income',
|
||||
).id;
|
||||
const accounts = await mainApp['api/accounts-get']();
|
||||
const payees = await mainApp['api/payees-get']();
|
||||
const accounts = await send('api/accounts-get');
|
||||
const payees = await send('api/payees-get');
|
||||
|
||||
function getCategory(id: string) {
|
||||
if (id == null || id === 'Category/__Split__') {
|
||||
@@ -245,7 +245,7 @@ async function importTransactions(
|
||||
})
|
||||
.filter(x => x);
|
||||
|
||||
await mainApp['api/transactions-add']({
|
||||
await send('api/transactions-add', {
|
||||
accountId: entityIdMap.get(accountId),
|
||||
transactions: toImport,
|
||||
learnCategories: true,
|
||||
@@ -291,7 +291,7 @@ async function importBudgets(
|
||||
) {
|
||||
const budgets = sortByKey(data.monthlyBudgets, 'month');
|
||||
|
||||
await mainApp['api/batch-budget-start']();
|
||||
await send('api/batch-budget-start');
|
||||
try {
|
||||
for (const budget of budgets) {
|
||||
const filled = fillInBudgets(
|
||||
@@ -308,20 +308,20 @@ async function importBudgets(
|
||||
return;
|
||||
}
|
||||
|
||||
await mainApp['api/budget-set-amount']({
|
||||
await send('api/budget-set-amount', {
|
||||
month,
|
||||
categoryId: catId,
|
||||
amount,
|
||||
});
|
||||
|
||||
if (catBudget.overspendingHandling === 'AffectsBuffer') {
|
||||
await mainApp['api/budget-set-carryover']({
|
||||
await send('api/budget-set-carryover', {
|
||||
month,
|
||||
categoryId: catId,
|
||||
flag: false,
|
||||
});
|
||||
} else if (catBudget.overspendingHandling === 'Confined') {
|
||||
await mainApp['api/budget-set-carryover']({
|
||||
await send('api/budget-set-carryover', {
|
||||
month,
|
||||
categoryId: catId,
|
||||
flag: true,
|
||||
@@ -331,7 +331,7 @@ async function importBudgets(
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await mainApp['api/batch-budget-end']();
|
||||
await send('api/batch-budget-end');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +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 { aqlQuery } from '../aql';
|
||||
import { mainApp } from '../main';
|
||||
import { send } from '../main-app';
|
||||
import { ruleModel } from '../transactions/transaction-rules';
|
||||
|
||||
import type {
|
||||
@@ -272,7 +271,7 @@ function importAccounts(data: Budget, entityIdMap: Map<string, string>) {
|
||||
return Promise.all(
|
||||
data.accounts.map(async account => {
|
||||
if (!account.deleted) {
|
||||
const id = await mainApp['api/account-create']({
|
||||
const id = await send('api/account-create', {
|
||||
account: {
|
||||
name: account.name,
|
||||
offbudget: account.on_budget ? false : true,
|
||||
@@ -292,7 +291,7 @@ async function importCategories(
|
||||
// Hidden categories are put in its own group by YNAB,
|
||||
// so it's already handled.
|
||||
|
||||
const categories = await mainApp['api/categories-get']({
|
||||
const categories = await send('api/categories-get', {
|
||||
grouped: false,
|
||||
});
|
||||
const incomeCatId = findIdByName(categories, 'Income');
|
||||
@@ -337,7 +336,7 @@ async function importCategories(
|
||||
while (true) {
|
||||
const name = count === 0 ? baseName : `${baseName} (${count})`;
|
||||
try {
|
||||
const id = await mainApp['api/category-group-create']({
|
||||
const id = await send('api/category-group-create', {
|
||||
group: { ...params, name },
|
||||
});
|
||||
return { id, name };
|
||||
@@ -362,7 +361,7 @@ async function importCategories(
|
||||
while (true) {
|
||||
const name = count === 0 ? baseName : `${baseName} (${count})`;
|
||||
try {
|
||||
const id = await mainApp['api/category-create']({
|
||||
const id = await send('api/category-create', {
|
||||
category: { ...params, name },
|
||||
});
|
||||
return { id, name };
|
||||
@@ -394,7 +393,7 @@ async function importCategories(
|
||||
groupId = createdGroup.id;
|
||||
entityIdMap.set(group.id, groupId);
|
||||
if (group.note) {
|
||||
void mainApp['notes-save']({
|
||||
void send('notes-save', {
|
||||
id: groupId,
|
||||
note: group.note,
|
||||
});
|
||||
@@ -435,7 +434,7 @@ async function importCategories(
|
||||
});
|
||||
entityIdMap.set(cat.id, createdCategory.id);
|
||||
if (cat.note) {
|
||||
void mainApp['notes-save']({
|
||||
void send('notes-save', {
|
||||
id: createdCategory.id,
|
||||
note: cat.note,
|
||||
});
|
||||
@@ -452,7 +451,7 @@ function importPayees(data: Budget, entityIdMap: Map<string, string>) {
|
||||
return Promise.all(
|
||||
data.payees.map(async payee => {
|
||||
if (!payee.deleted) {
|
||||
const id = await mainApp['api/payee-create']({
|
||||
const id = await send('api/payee-create', {
|
||||
payee: { name: payee.name },
|
||||
});
|
||||
entityIdMap.set(payee.id, id);
|
||||
@@ -499,7 +498,7 @@ async function importPayeeLocations(
|
||||
|
||||
try {
|
||||
// Create the payee location in Actual
|
||||
await mainApp.createPayeeLocation({
|
||||
await send('payee-location-create', {
|
||||
payeeId: actualPayeeId,
|
||||
latitude,
|
||||
longitude,
|
||||
@@ -558,7 +557,7 @@ async function importFlagsAsTags(
|
||||
|
||||
await Promise.all(
|
||||
[...tagsToCreate.entries()].map(async ([tag, color]) => {
|
||||
await mainApp['tags-create']({
|
||||
await send('tags-create', {
|
||||
tag,
|
||||
color,
|
||||
description: 'Imported from YNAB',
|
||||
@@ -572,8 +571,8 @@ async function importTransactions(
|
||||
entityIdMap: Map<string, string>,
|
||||
flagNameConflicts: Set<string>,
|
||||
) {
|
||||
const payees = await mainApp['api/payees-get']();
|
||||
const categories = await mainApp['api/categories-get']({
|
||||
const payees = await send('api/payees-get');
|
||||
const categories = await send('api/categories-get', {
|
||||
grouped: false,
|
||||
});
|
||||
const incomeCatId = findIdByName(categories, 'Income');
|
||||
@@ -838,7 +837,7 @@ async function importTransactions(
|
||||
})
|
||||
.filter(x => x);
|
||||
|
||||
await mainApp['api/transactions-add']({
|
||||
await send('api/transactions-add', {
|
||||
accountId: entityIdMap.get(accountId),
|
||||
transactions: toImport,
|
||||
learnCategories: true,
|
||||
@@ -862,7 +861,7 @@ async function importScheduledTransactions(
|
||||
return;
|
||||
}
|
||||
|
||||
const payees = await mainApp['api/payees-get']();
|
||||
const payees = await send('api/payees-get');
|
||||
const payeesByTransferAcct = payees
|
||||
.filter(payee => payee?.transfer_acct)
|
||||
.map(payee => [payee.transfer_acct, payee] as [string, Payee]);
|
||||
@@ -885,7 +884,7 @@ async function importScheduledTransactions(
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return await mainApp['api/schedule-create']({
|
||||
return await send('api/schedule-create', {
|
||||
...params,
|
||||
name: params.name,
|
||||
});
|
||||
@@ -903,16 +902,19 @@ async function importScheduledTransactions(
|
||||
async function getRuleForSchedule(
|
||||
scheduleId: string,
|
||||
): Promise<RuleEntity | null> {
|
||||
const { data: ruleId } = (await aqlQuery(
|
||||
q('schedules').filter({ id: scheduleId }).calculate('rule').serialize(),
|
||||
)) 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 aqlQuery(
|
||||
q('rules').filter({ id: ruleId }).select('*').serialize(),
|
||||
)) as { data: Array<Record<string, unknown>> };
|
||||
const { data: ruleData } = (await send('api/query', {
|
||||
query: q('rules').filter({ id: ruleId }).select('*').serialize(),
|
||||
})) as { data: Array<Record<string, unknown>> };
|
||||
const ruleRow = ruleData?.[0];
|
||||
if (!ruleRow) {
|
||||
return null;
|
||||
@@ -971,7 +973,7 @@ async function importScheduledTransactions(
|
||||
value: scheduleNotes,
|
||||
});
|
||||
|
||||
await mainApp['api/rule-update']({
|
||||
await send('api/rule-update', {
|
||||
rule: buildRuleUpdate(rule, actions),
|
||||
});
|
||||
}
|
||||
@@ -1006,7 +1008,7 @@ async function importScheduledTransactions(
|
||||
value: categoryId,
|
||||
});
|
||||
|
||||
await mainApp['api/rule-update']({
|
||||
await send('api/rule-update', {
|
||||
rule: buildRuleUpdate(rule, actions),
|
||||
});
|
||||
}
|
||||
@@ -1085,7 +1087,7 @@ async function importScheduledTransactions(
|
||||
}
|
||||
});
|
||||
|
||||
await mainApp['api/rule-update']({
|
||||
await send('api/rule-update', {
|
||||
rule: buildRuleUpdate(rule, actions),
|
||||
});
|
||||
}
|
||||
@@ -1112,7 +1114,7 @@ async function importBudgets(data: Budget, entityIdMap: Map<string, string>) {
|
||||
'Credit Card Payments',
|
||||
);
|
||||
|
||||
await mainApp['api/batch-budget-start']();
|
||||
await send('api/batch-budget-start');
|
||||
try {
|
||||
for (const budget of budgets) {
|
||||
const month = monthUtils.monthFromDate(budget.month);
|
||||
@@ -1130,7 +1132,7 @@ async function importBudgets(data: Budget, entityIdMap: Map<string, string>) {
|
||||
return;
|
||||
}
|
||||
|
||||
await mainApp['api/budget-set-amount']({
|
||||
await send('api/budget-set-amount', {
|
||||
month,
|
||||
categoryId: catId,
|
||||
amount,
|
||||
@@ -1139,7 +1141,7 @@ async function importBudgets(data: Budget, entityIdMap: Map<string, string>) {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await mainApp['api/batch-budget-end']();
|
||||
await send('api/batch-budget-end');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
packages/loot-core/src/server/main-app.ts
Normal file
25
packages/loot-core/src/server/main-app.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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<Handlers>();
|
||||
|
||||
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<K extends keyof Handlers>(
|
||||
name: K,
|
||||
args?: Parameters<Handlers[K]>[0],
|
||||
): Promise<Awaited<ReturnType<Handlers[K]>>> {
|
||||
return runHandler(app.handlers[name], args, { name }) as Promise<
|
||||
Awaited<ReturnType<Handlers[K]>>
|
||||
>;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import * as monthUtils from '../shared/months';
|
||||
import * as budgetActions from './budget/actions';
|
||||
import * as budget from './budget/base';
|
||||
import * as db from './db';
|
||||
import { mainApp } from './main';
|
||||
import { handlers } from './main';
|
||||
import {
|
||||
disableGlobalMutations,
|
||||
enableGlobalMutations,
|
||||
@@ -28,7 +28,7 @@ beforeEach(async () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await mainApp['close-budget']();
|
||||
await runHandler(handlers['close-budget']);
|
||||
connection.resetEvents();
|
||||
enableGlobalMutations();
|
||||
global.currentMonth = null;
|
||||
@@ -72,7 +72,7 @@ describe('Budgets', () => {
|
||||
'SELECT * FROM messages_clock',
|
||||
);
|
||||
|
||||
const { error } = await mainApp['load-budget']({
|
||||
const { error } = await runHandler(handlers['load-budget'], {
|
||||
id: 'test-budget',
|
||||
});
|
||||
expect(error).toBe(undefined);
|
||||
@@ -92,7 +92,7 @@ describe('Budgets', () => {
|
||||
|
||||
const spy = vi.spyOn(console, 'warn').mockImplementation(() => null);
|
||||
|
||||
const { error } = await mainApp['load-budget']({
|
||||
const { error } = await runHandler(handlers['load-budget'], {
|
||||
id: 'test-budget',
|
||||
});
|
||||
// There should be an error and the budget should be unloaded
|
||||
@@ -128,7 +128,7 @@ describe('Accounts', () => {
|
||||
});
|
||||
|
||||
const id = 'test-transfer';
|
||||
await mainApp['transaction-add']({
|
||||
await runHandler(handlers['transaction-add'], {
|
||||
id,
|
||||
account: 'one',
|
||||
amount: 5000,
|
||||
@@ -140,7 +140,7 @@ describe('Accounts', () => {
|
||||
);
|
||||
|
||||
let transaction = await db.getTransaction(id);
|
||||
await mainApp['transaction-update']({
|
||||
await runHandler(handlers['transaction-update'], {
|
||||
...(await db.getTransaction(id)),
|
||||
payee: 'transfer-three',
|
||||
date: '2017-01-03',
|
||||
@@ -150,7 +150,7 @@ describe('Accounts', () => {
|
||||
);
|
||||
|
||||
transaction = await db.getTransaction(id);
|
||||
await mainApp['transaction-delete'](transaction);
|
||||
await runHandler(handlers['transaction-delete'], transaction);
|
||||
differ.expectToMatchDiff(
|
||||
await db.all<db.DbTransaction>('SELECT * FROM transactions'),
|
||||
);
|
||||
@@ -172,7 +172,7 @@ describe('Budget', () => {
|
||||
await db.insertCategory({ name: 'bar', cat_group: 'group1' });
|
||||
});
|
||||
|
||||
let bounds = await mainApp['get-budget-bounds']();
|
||||
let bounds = await runHandler(handlers['get-budget-bounds']);
|
||||
expect(bounds.start).toBe('2016-10');
|
||||
expect(bounds.end).toBe('2018-01');
|
||||
expect(spreadsheet.meta().createdMonths).toMatchSnapshot();
|
||||
@@ -181,7 +181,7 @@ describe('Budget', () => {
|
||||
// current earliest budget to test if it creates the necessary
|
||||
// budgets for the earlier months
|
||||
db.runQuery("INSERT INTO accounts (id, name) VALUES ('one', 'boa')");
|
||||
await mainApp['transaction-add']({
|
||||
await runHandler(handlers['transaction-add'], {
|
||||
id: uuidv4(),
|
||||
date: '2016-05-06',
|
||||
amount: 50,
|
||||
@@ -192,7 +192,7 @@ describe('Budget', () => {
|
||||
// budgets for the months in the future
|
||||
global.currentMonth = '2017-02';
|
||||
|
||||
bounds = await mainApp['get-budget-bounds']();
|
||||
bounds = await runHandler(handlers['get-budget-bounds']);
|
||||
expect(bounds.start).toBe('2016-02');
|
||||
expect(bounds.end).toBe('2018-02');
|
||||
expect(spreadsheet.meta().createdMonths).toMatchSnapshot();
|
||||
@@ -230,19 +230,19 @@ describe('Budget', () => {
|
||||
db.insertCategoryGroup({ id: 'group1', name: 'group1' }),
|
||||
);
|
||||
categories = [
|
||||
await mainApp['category-create']({
|
||||
await runHandler(handlers['category-create'], {
|
||||
name: 'foo',
|
||||
groupId: 'group1',
|
||||
}),
|
||||
await mainApp['category-create']({
|
||||
await runHandler(handlers['category-create'], {
|
||||
name: 'bar',
|
||||
groupId: 'group1',
|
||||
}),
|
||||
await mainApp['category-create']({
|
||||
await runHandler(handlers['category-create'], {
|
||||
name: 'baz',
|
||||
groupId: 'group1',
|
||||
}),
|
||||
await mainApp['category-create']({
|
||||
await runHandler(handlers['category-create'], {
|
||||
name: 'biz',
|
||||
groupId: 'group1',
|
||||
}),
|
||||
@@ -259,14 +259,14 @@ describe('Budget', () => {
|
||||
};
|
||||
// Test insertions
|
||||
let changed = await captureChangedCells(() =>
|
||||
mainApp['transaction-add'](trans),
|
||||
runHandler(handlers['transaction-add'], trans),
|
||||
);
|
||||
expect(
|
||||
changed.sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)),
|
||||
).toMatchSnapshot();
|
||||
// Test updates
|
||||
changed = await captureChangedCells(async () => {
|
||||
await mainApp['transaction-update']({
|
||||
await runHandler(handlers['transaction-update'], {
|
||||
...(await db.getTransaction(trans.id)),
|
||||
amount: 7000,
|
||||
});
|
||||
@@ -276,7 +276,7 @@ describe('Budget', () => {
|
||||
).toMatchSnapshot();
|
||||
// Test deletions
|
||||
changed = await captureChangedCells(async () => {
|
||||
await mainApp['transaction-delete']({ id: trans.id });
|
||||
await runHandler(handlers['transaction-delete'], { id: trans.id });
|
||||
});
|
||||
expect(
|
||||
changed.sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)),
|
||||
@@ -298,7 +298,7 @@ describe('Categories', () => {
|
||||
expect(categories.length).toBe(2);
|
||||
expect(categories.find(cat => cat.name === 'foo')).not.toBeNull();
|
||||
expect(categories.find(cat => cat.name === 'bar')).not.toBeNull();
|
||||
await mainApp['category-delete']({ id: 'foo' });
|
||||
await runHandler(handlers['category-delete'], { id: 'foo' });
|
||||
|
||||
categories = await db.getCategories();
|
||||
expect(categories.length).toBe(1);
|
||||
@@ -355,7 +355,7 @@ describe('Categories', () => {
|
||||
let trans = await db.getTransaction(transId);
|
||||
expect(trans.category).toBe('foo');
|
||||
|
||||
await mainApp['category-delete']({
|
||||
await runHandler(handlers['category-delete'], {
|
||||
id: 'foo',
|
||||
transferId: 'bar',
|
||||
});
|
||||
@@ -371,7 +371,7 @@ describe('Categories', () => {
|
||||
// Transfering an income category to an expense just doesn't make
|
||||
// sense. Make sure this doesn't do anything.
|
||||
await expect(
|
||||
mainApp['category-delete']({
|
||||
runHandler(handlers['category-delete'], {
|
||||
id: 'income1',
|
||||
transferId: 'bar',
|
||||
}),
|
||||
@@ -381,7 +381,7 @@ describe('Categories', () => {
|
||||
expect(categories.find(cat => cat.id === 'income1')).toBeDefined();
|
||||
|
||||
// Make sure you can delete income categories
|
||||
await mainApp['category-delete']({
|
||||
await runHandler(handlers['category-delete'], {
|
||||
id: 'income1',
|
||||
transferId: 'income2',
|
||||
});
|
||||
|
||||
@@ -6,15 +6,12 @@ import * as fs from '../platform/server/fs';
|
||||
import { logger, setVerboseMode } from '../platform/server/log';
|
||||
import * as sqlite from '../platform/server/sqlite';
|
||||
import { q } from '../shared/query';
|
||||
import type { QueryState } from '../shared/query';
|
||||
import { amountToInteger, integerToAmount } from '../shared/util';
|
||||
import type { ApiHandlers } from '../types/api-handlers';
|
||||
import type { Handlers, ServerHandlers } from '../types/handlers';
|
||||
import type { Handlers } from '../types/handlers';
|
||||
|
||||
import { app as accountsApp } from './accounts/app';
|
||||
import { app as adminApp } from './admin/app';
|
||||
import { app as apiApp } from './api';
|
||||
import { createApp } from './app';
|
||||
import { installAPI } from './api';
|
||||
import { aqlQuery } from './aql';
|
||||
import { app as authApp } from './auth/app';
|
||||
import { app as budgetApp } from './budget/app';
|
||||
@@ -24,7 +21,8 @@ import * as db from './db';
|
||||
import * as encryption from './encryption';
|
||||
import { app as encryptionApp } from './encryption/app';
|
||||
import { app as filtersApp } from './filters/app';
|
||||
import { mutator } from './mutators';
|
||||
import { app } from './main-app';
|
||||
import { mutator, runHandler } from './mutators';
|
||||
import { app as notesApp } from './notes/app';
|
||||
import { app as payeesApp } from './payees/app';
|
||||
import { get } from './post';
|
||||
@@ -43,24 +41,38 @@ import { app as transactionsApp } from './transactions/app';
|
||||
import * as rules from './transactions/transaction-rules';
|
||||
import { redo, undo } from './undo';
|
||||
|
||||
async function makeFiltersFromConditions({
|
||||
// handlers
|
||||
|
||||
// need to work around the type system here because the object
|
||||
// is /currently/ empty but we promise to fill it in later
|
||||
export let handlers = {} as unknown as Handlers;
|
||||
|
||||
handlers['undo'] = mutator(async function () {
|
||||
return undo();
|
||||
});
|
||||
|
||||
handlers['redo'] = mutator(function () {
|
||||
return redo();
|
||||
});
|
||||
|
||||
handlers['make-filters-from-conditions'] = async function ({
|
||||
conditions,
|
||||
applySpecialCases = undefined,
|
||||
applySpecialCases,
|
||||
}) {
|
||||
return rules.conditionsToAQL(conditions, { applySpecialCases });
|
||||
}
|
||||
};
|
||||
|
||||
async function query(query) {
|
||||
handlers['query'] = async function (query) {
|
||||
if (query['table'] == null) {
|
||||
throw new Error('query has no table, did you forgot to call `.serialize`?');
|
||||
}
|
||||
|
||||
return aqlQuery(query);
|
||||
}
|
||||
};
|
||||
|
||||
async function getServerVersion() {
|
||||
handlers['get-server-version'] = async function () {
|
||||
if (!getServer()) {
|
||||
return { error: 'no-server' as const };
|
||||
return { error: 'no-server' };
|
||||
}
|
||||
|
||||
let version;
|
||||
@@ -68,19 +80,19 @@ async function getServerVersion() {
|
||||
const res = await get(getServer().BASE_SERVER + '/info');
|
||||
|
||||
const info = JSON.parse(res);
|
||||
version = info.build.version as string;
|
||||
version = info.build.version;
|
||||
} catch {
|
||||
return { error: 'network-failure' as const };
|
||||
return { error: 'network-failure' };
|
||||
}
|
||||
|
||||
return { version };
|
||||
}
|
||||
};
|
||||
|
||||
async function getServerUrl() {
|
||||
handlers['get-server-url'] = async function () {
|
||||
return getServer() && getServer().BASE_SERVER;
|
||||
}
|
||||
};
|
||||
|
||||
async function setServerUrl({ url, validate = true }) {
|
||||
handlers['set-server-url'] = async function ({ url, validate = true }) {
|
||||
if (url == null) {
|
||||
await asyncStorage.removeItem('user-token');
|
||||
} else {
|
||||
@@ -88,7 +100,7 @@ async function setServerUrl({ url, validate = true }) {
|
||||
|
||||
if (validate) {
|
||||
// Validate the server is running
|
||||
const result = await mainApp['subscribe-needs-bootstrap']({
|
||||
const result = await runHandler(handlers['subscribe-needs-bootstrap'], {
|
||||
url,
|
||||
});
|
||||
if ('error' in result) {
|
||||
@@ -101,60 +113,20 @@ async function setServerUrl({ url, validate = true }) {
|
||||
await asyncStorage.setItem('did-bootstrap', true);
|
||||
setServer(url);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
async function appFocused() {
|
||||
handlers['app-focused'] = async function () {
|
||||
if (prefs.getPrefs() && prefs.getPrefs().id) {
|
||||
// First we sync
|
||||
void fullSync();
|
||||
}
|
||||
}
|
||||
|
||||
export type MiscHandlers = {
|
||||
undo: () => Promise<void>;
|
||||
redo: () => Promise<void>;
|
||||
|
||||
'make-filters-from-conditions': (arg: {
|
||||
conditions: unknown;
|
||||
applySpecialCases?: boolean;
|
||||
}) => Promise<{ filters: unknown[] }>;
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
query: (query: QueryState) => Promise<{ data: any; dependencies: string[] }>;
|
||||
|
||||
'get-server-version': () => Promise<
|
||||
{ error: 'no-server' } | { error: 'network-failure' } | { version: string }
|
||||
>;
|
||||
|
||||
'get-server-url': () => Promise<string | null>;
|
||||
|
||||
'set-server-url': (arg: {
|
||||
url: string;
|
||||
validate?: boolean;
|
||||
}) => Promise<{ error?: string }>;
|
||||
|
||||
'app-focused': () => Promise<void>;
|
||||
};
|
||||
|
||||
const miscApp = createApp<MiscHandlers>({
|
||||
undo: mutator(undo),
|
||||
redo: mutator(redo),
|
||||
'make-filters-from-conditions': makeFiltersFromConditions,
|
||||
query,
|
||||
'get-server-version': getServerVersion,
|
||||
'get-server-url': getServerUrl,
|
||||
'set-server-url': setServerUrl,
|
||||
'app-focused': appFocused,
|
||||
});
|
||||
handlers = installAPI(handlers) as Handlers;
|
||||
|
||||
const serverApp = createApp<ServerHandlers>();
|
||||
|
||||
serverApp.events.on('sync', event => {
|
||||
connection.send('sync-event', event);
|
||||
});
|
||||
|
||||
serverApp.combine(
|
||||
miscApp,
|
||||
// A hack for now until we clean up everything
|
||||
app.handlers = handlers;
|
||||
app.combine(
|
||||
authApp,
|
||||
schedulesApp,
|
||||
budgetApp,
|
||||
@@ -176,9 +148,6 @@ serverApp.combine(
|
||||
tagsApp,
|
||||
);
|
||||
|
||||
export const mainApp = createApp<Handlers>();
|
||||
mainApp.combine(apiApp, serverApp);
|
||||
|
||||
export function getDefaultDocumentDir() {
|
||||
return fs.join(process.env.ACTUAL_DOCUMENT_DIR, 'Actual');
|
||||
}
|
||||
@@ -240,7 +209,17 @@ export async function initApp(isDev, socketName) {
|
||||
}
|
||||
setServer(url);
|
||||
|
||||
connection.init(socketName, mainApp);
|
||||
connection.init(socketName, app.handlers);
|
||||
|
||||
// Allow running DB queries locally
|
||||
global.$query = aqlQuery;
|
||||
global.$q = q;
|
||||
|
||||
if (isDev) {
|
||||
global.$send = (name, args) => runHandler(app.handlers[name], args);
|
||||
global.$db = db;
|
||||
global.$setSyncingMode = setSyncingMode;
|
||||
}
|
||||
}
|
||||
|
||||
type BaseInitConfig = {
|
||||
@@ -299,25 +278,25 @@ export async function init(config: InitConfig) {
|
||||
|
||||
if ('sessionToken' in config && config.sessionToken) {
|
||||
// Session token authentication
|
||||
await mainApp['subscribe-set-token']({
|
||||
await runHandler(handlers['subscribe-set-token'], {
|
||||
token: config.sessionToken,
|
||||
});
|
||||
// Validate the token
|
||||
const user = await mainApp['subscribe-get-user']();
|
||||
const user = await runHandler(handlers['subscribe-get-user'], undefined);
|
||||
if (!user || user.tokenExpired === true) {
|
||||
// Clear invalid token
|
||||
await mainApp['subscribe-set-token']({ token: '' });
|
||||
await runHandler(handlers['subscribe-set-token'], { token: '' });
|
||||
throw new Error(
|
||||
'Authentication failed: invalid or expired session token',
|
||||
);
|
||||
}
|
||||
if (user.offline === true) {
|
||||
// Clear token since we can't validate
|
||||
await mainApp['subscribe-set-token']({ token: '' });
|
||||
await runHandler(handlers['subscribe-set-token'], { token: '' });
|
||||
throw new Error('Authentication failed: server offline or unreachable');
|
||||
}
|
||||
} else if ('password' in config && config.password) {
|
||||
const result = await mainApp['subscribe-sign-in']({
|
||||
const result = await runHandler(handlers['subscribe-sign-in'], {
|
||||
password: config.password,
|
||||
});
|
||||
if (result?.error) {
|
||||
@@ -329,7 +308,7 @@ export async function init(config: InitConfig) {
|
||||
// access to the server, we are doing things locally
|
||||
setServer(null);
|
||||
|
||||
mainApp.events.on('load-budget', () => {
|
||||
app.events.on('load-budget', () => {
|
||||
setSyncingMode('offline');
|
||||
});
|
||||
}
|
||||
@@ -342,14 +321,14 @@ export async function init(config: InitConfig) {
|
||||
export const lib = {
|
||||
getDataDir: fs.getDataDir,
|
||||
sendMessage: (msg, args) => connection.send(msg, args),
|
||||
send: async <K extends keyof Handlers>(
|
||||
send: async <K extends keyof Handlers, T extends Handlers[K]>(
|
||||
name: K,
|
||||
args?: Parameters<Handlers[K]>[0],
|
||||
): Promise<Awaited<ReturnType<Handlers[K]>>> => {
|
||||
const res = await mainApp.runHandler(name, args);
|
||||
return res as Awaited<ReturnType<Handlers[K]>>;
|
||||
args?: Parameters<T>[0],
|
||||
): Promise<Awaited<ReturnType<T>>> => {
|
||||
const res = await runHandler(app.handlers[name], args);
|
||||
return res;
|
||||
},
|
||||
on: (name, func) => mainApp.events.on(name, func),
|
||||
on: (name, func) => app.events.on(name, func),
|
||||
q,
|
||||
db,
|
||||
amountToInteger,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { captureBreadcrumb, captureException } from '../platform/exceptions';
|
||||
import { sequential } from '../shared/async';
|
||||
|
||||
type Handler<TArgs extends unknown[] = unknown[], TReturn = unknown> = (
|
||||
...args: TArgs
|
||||
) => TReturn;
|
||||
import type { HandlerFunctions, Handlers } from '../types/handlers';
|
||||
|
||||
const runningMethods = new Set();
|
||||
|
||||
@@ -14,9 +11,7 @@ let globalMutationsEnabled = false;
|
||||
|
||||
let _latestHandlerNames = [];
|
||||
|
||||
export function mutator<TArgs extends unknown[], TReturn>(
|
||||
handler: Handler<TArgs, TReturn>,
|
||||
): Handler<TArgs, TReturn> {
|
||||
export function mutator<T extends HandlerFunctions>(handler: T): T {
|
||||
mutatingMethods.set(handler, true);
|
||||
return handler;
|
||||
}
|
||||
@@ -43,11 +38,11 @@ function wait(time) {
|
||||
return new Promise(resolve => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
export async function runHandler<TArgs extends unknown[], TReturn>(
|
||||
handler: Handler<TArgs, Promise<TReturn>>,
|
||||
args?: TArgs[0],
|
||||
export async function runHandler<T extends Handlers[keyof Handlers]>(
|
||||
handler: T,
|
||||
args?: Parameters<T>[0],
|
||||
{ undoTag, name }: { undoTag?; name? } = {},
|
||||
): Promise<TReturn> {
|
||||
): Promise<ReturnType<T>> {
|
||||
// For debug reasons, track the latest handlers that have been
|
||||
// called
|
||||
_latestHandlerNames.push(name);
|
||||
@@ -55,16 +50,10 @@ export async function runHandler<TArgs extends unknown[], TReturn>(
|
||||
_latestHandlerNames = _latestHandlerNames.slice(-5);
|
||||
}
|
||||
|
||||
const invokeHandler = () =>
|
||||
handler(...((args !== undefined ? [args] : []) as TArgs));
|
||||
|
||||
if (mutatingMethods.has(handler)) {
|
||||
// If already inside a mutator, call directly to avoid deadlocking the
|
||||
// sequential queue.
|
||||
if (currentContext !== null) {
|
||||
return invokeHandler();
|
||||
}
|
||||
return runMutator(invokeHandler, { undoTag });
|
||||
return runMutator(() => handler(args), { undoTag }) as Promise<
|
||||
ReturnType<T>
|
||||
>;
|
||||
}
|
||||
|
||||
// When closing a file, it clears out all global state for the file. That
|
||||
@@ -75,12 +64,12 @@ export async function runHandler<TArgs extends unknown[], TReturn>(
|
||||
await flushRunningMethods();
|
||||
}
|
||||
|
||||
const promise = invokeHandler();
|
||||
const promise = handler(args);
|
||||
runningMethods.add(promise);
|
||||
void promise.then(() => {
|
||||
runningMethods.delete(promise);
|
||||
});
|
||||
return promise;
|
||||
return promise as Promise<ReturnType<T>>;
|
||||
}
|
||||
|
||||
// These are useful for tests. Only use them in tests.
|
||||
|
||||
@@ -14,42 +14,44 @@ import { batchMessages } from '../sync';
|
||||
import * as rules from '../transactions/transaction-rules';
|
||||
import { undoable } from '../undo';
|
||||
|
||||
export type PayeeHandlers = {
|
||||
createPayee: typeof createPayee;
|
||||
getCommonPayees: typeof getCommonPayees;
|
||||
getPayees: typeof getPayees;
|
||||
getOrphanedPayees: typeof getOrphanedPayees;
|
||||
getPayeeRuleCounts: typeof getPayeeRuleCounts;
|
||||
mergePayees: typeof mergePayees;
|
||||
batchChangePayees: typeof batchChangePayees;
|
||||
checkOrphanedPayees: typeof checkOrphanedPayees;
|
||||
getPayeeRules: typeof getPayeeRules;
|
||||
createPayeeLocation: typeof createPayeeLocation;
|
||||
getPayeeLocations: typeof getPayeeLocations;
|
||||
deletePayeeLocation: typeof deletePayeeLocation;
|
||||
getNearbyPayees: typeof getNearbyPayees;
|
||||
export type PayeesHandlers = {
|
||||
'payee-create': typeof createPayee;
|
||||
'common-payees-get': typeof getCommonPayees;
|
||||
'payees-get': typeof getPayees;
|
||||
'payees-get-orphaned': typeof getOrphanedPayees;
|
||||
'payees-get-rule-counts': typeof getPayeeRuleCounts;
|
||||
'payees-merge': typeof mergePayees;
|
||||
'payees-batch-change': typeof batchChangePayees;
|
||||
'payees-check-orphaned': typeof checkOrphanedPayees;
|
||||
'payees-get-rules': typeof getPayeeRules;
|
||||
'payee-location-create': typeof createPayeeLocation;
|
||||
'payee-locations-get': typeof getPayeeLocations;
|
||||
'payee-location-delete': typeof deletePayeeLocation;
|
||||
'payees-get-nearby': typeof getNearbyPayees;
|
||||
};
|
||||
|
||||
export const app = createApp<PayeeHandlers>({
|
||||
createPayee: mutator(undoable(createPayee)),
|
||||
batchChangePayees: mutator(undoable(batchChangePayees)),
|
||||
createPayeeLocation: mutator(createPayeeLocation),
|
||||
deletePayeeLocation: mutator(deletePayeeLocation),
|
||||
mergePayees: mutator(
|
||||
export const app = createApp<PayeesHandlers>();
|
||||
app.method('payee-create', mutator(undoable(createPayee)));
|
||||
app.method('common-payees-get', getCommonPayees);
|
||||
app.method('payees-get', getPayees);
|
||||
app.method('payees-get-orphaned', getOrphanedPayees);
|
||||
app.method('payees-get-rule-counts', getPayeeRuleCounts);
|
||||
app.method(
|
||||
'payees-merge',
|
||||
mutator(
|
||||
undoable(mergePayees, args => ({
|
||||
mergeIds: args.mergeIds,
|
||||
targetId: args.targetId,
|
||||
})),
|
||||
),
|
||||
getCommonPayees,
|
||||
getPayees,
|
||||
getOrphanedPayees,
|
||||
getPayeeRuleCounts,
|
||||
checkOrphanedPayees,
|
||||
getPayeeRules,
|
||||
getPayeeLocations,
|
||||
getNearbyPayees,
|
||||
});
|
||||
);
|
||||
app.method('payees-batch-change', mutator(undoable(batchChangePayees)));
|
||||
app.method('payees-check-orphaned', checkOrphanedPayees);
|
||||
app.method('payees-get-rules', getPayeeRules);
|
||||
app.method('payee-location-create', mutator(createPayeeLocation));
|
||||
app.method('payee-locations-get', getPayeeLocations);
|
||||
app.method('payee-location-delete', mutator(deletePayeeLocation));
|
||||
app.method('payees-get-nearby', getNearbyPayees);
|
||||
|
||||
async function createPayee({ name }: { name: PayeeEntity['name'] }) {
|
||||
return db.insertPayee({ name });
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { MetadataPrefs } from '../../types/prefs';
|
||||
import { setType as setBudgetType, triggerBudgetChanges } from '../budget/base';
|
||||
import * as db from '../db';
|
||||
import { PostError, SyncError } from '../errors';
|
||||
import { mainApp } from '../main';
|
||||
import { app } from '../main-app';
|
||||
import { runMutator } from '../mutators';
|
||||
import { postBinary } from '../post';
|
||||
import * as prefs from '../prefs';
|
||||
@@ -410,7 +410,7 @@ export const applyMessages = sequential(async (messages: Message[]) => {
|
||||
_syncListeners.forEach(func => func(oldData, newData));
|
||||
|
||||
const tables = getTablesFromMessages(messages.filter(msg => !msg.old));
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'applied',
|
||||
tables,
|
||||
data: newData,
|
||||
@@ -444,16 +444,16 @@ async function errorHandler(e: Error) {
|
||||
// couldn't apply, which doesn't make any sense. Must be a bug
|
||||
// in the code. Send a specific error type for it for a custom
|
||||
// message.
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'error',
|
||||
subtype: 'apply-failure',
|
||||
meta: e.meta,
|
||||
});
|
||||
} else {
|
||||
mainApp.events.emit('sync', { type: 'error', meta: e.meta });
|
||||
app.events.emit('sync', { type: 'error', meta: e.meta });
|
||||
}
|
||||
} else if (e instanceof Timestamp.ClockDriftError) {
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'error',
|
||||
subtype: 'clock-drift',
|
||||
meta: { message: e.message },
|
||||
@@ -574,7 +574,7 @@ export const fullSync = once(async function (): Promise<
|
||||
| { messages: Message[] }
|
||||
| { error: { message: string; reason: string; meta: unknown } }
|
||||
> {
|
||||
mainApp.events.emit('sync', { type: 'start' });
|
||||
app.events.emit('sync', { type: 'start' });
|
||||
let messages;
|
||||
|
||||
try {
|
||||
@@ -586,13 +586,13 @@ export const fullSync = once(async function (): Promise<
|
||||
if (e.reason === 'out-of-sync') {
|
||||
captureException(e);
|
||||
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'error',
|
||||
subtype: 'out-of-sync',
|
||||
meta: e.meta,
|
||||
});
|
||||
} else if (e.reason === 'invalid-schema') {
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'error',
|
||||
subtype: 'invalid-schema',
|
||||
meta: e.meta,
|
||||
@@ -601,36 +601,36 @@ export const fullSync = once(async function (): Promise<
|
||||
e.reason === 'decrypt-failure' ||
|
||||
e.reason === 'encrypt-failure'
|
||||
) {
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'error',
|
||||
subtype: e.reason,
|
||||
meta: e.meta,
|
||||
});
|
||||
} else if (e.reason === 'clock-drift') {
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'error',
|
||||
subtype: 'clock-drift',
|
||||
meta: e.meta,
|
||||
});
|
||||
} else {
|
||||
mainApp.events.emit('sync', { type: 'error', meta: e.meta });
|
||||
app.events.emit('sync', { type: 'error', meta: e.meta });
|
||||
}
|
||||
} else if (e instanceof PostError) {
|
||||
logger.log(e);
|
||||
if (e.reason === 'unauthorized') {
|
||||
mainApp.events.emit('sync', { type: 'unauthorized' });
|
||||
app.events.emit('sync', { type: 'unauthorized' });
|
||||
|
||||
// Set the user into read-only mode
|
||||
void asyncStorage.setItem('readOnly', 'true');
|
||||
} else if (e.reason === 'network-failure') {
|
||||
mainApp.events.emit('sync', { type: 'error', subtype: 'network' });
|
||||
app.events.emit('sync', { type: 'error', subtype: 'network' });
|
||||
} else {
|
||||
mainApp.events.emit('sync', { type: 'error', subtype: e.reason });
|
||||
app.events.emit('sync', { type: 'error', subtype: e.reason });
|
||||
}
|
||||
} else {
|
||||
captureException(e);
|
||||
// TODO: Send the message to the client and allow them to expand & view it
|
||||
mainApp.events.emit('sync', { type: 'error' });
|
||||
app.events.emit('sync', { type: 'error' });
|
||||
}
|
||||
|
||||
return { error: { message: e.message, reason: e.reason, meta: e.meta } };
|
||||
@@ -638,7 +638,7 @@ export const fullSync = once(async function (): Promise<
|
||||
|
||||
const tables = getTablesFromMessages(messages);
|
||||
|
||||
mainApp.events.emit('sync', {
|
||||
app.events.emit('sync', {
|
||||
type: 'success',
|
||||
tables,
|
||||
syncDisabled: checkSyncingMode('disabled'),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import * as fs from '../../platform/server/fs';
|
||||
import { mainApp } from '../main';
|
||||
import { handlers } from '../main';
|
||||
|
||||
export async function uniqueBudgetName(
|
||||
initialName: string = 'My Finances',
|
||||
): Promise<string> {
|
||||
const budgets = await mainApp['get-budgets']();
|
||||
const budgets = await handlers['get-budgets']();
|
||||
let idx = 1;
|
||||
|
||||
// If there is a conflict, keep appending an index until there is no
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { App } from '../server/app';
|
||||
@@ -6,9 +6,8 @@ import type { BudgetFileHandlers } from '../server/budgetfiles/app';
|
||||
import type { DashboardHandlers } from '../server/dashboard/app';
|
||||
import type { EncryptionHandlers } from '../server/encryption/app';
|
||||
import type { FiltersHandlers } from '../server/filters/app';
|
||||
import type { MiscHandlers } from '../server/main';
|
||||
import type { NotesHandlers } from '../server/notes/app';
|
||||
import type { PayeeHandlers } from '../server/payees/app';
|
||||
import type { PayeesHandlers } from '../server/payees/app';
|
||||
import type { PreferencesHandlers } from '../server/preferences/app';
|
||||
import type { ReportsHandlers } from '../server/reports/app';
|
||||
import type { RulesHandlers } from '../server/rules/app';
|
||||
@@ -20,8 +19,10 @@ import type { ToolsHandlers } from '../server/tools/app';
|
||||
import type { TransactionHandlers } from '../server/transactions/app';
|
||||
|
||||
import type { ApiHandlers } from './api-handlers';
|
||||
import type { ServerHandlers } from './server-handlers';
|
||||
|
||||
export type ServerHandlers = MiscHandlers &
|
||||
export type Handlers = {} & ServerHandlers &
|
||||
ApiHandlers &
|
||||
BudgetHandlers &
|
||||
DashboardHandlers &
|
||||
FiltersHandlers &
|
||||
@@ -34,7 +35,7 @@ export type ServerHandlers = MiscHandlers &
|
||||
AdminHandlers &
|
||||
ToolsHandlers &
|
||||
AccountHandlers &
|
||||
PayeeHandlers &
|
||||
PayeesHandlers &
|
||||
SpreadsheetHandlers &
|
||||
SyncHandlers &
|
||||
BudgetFileHandlers &
|
||||
@@ -42,6 +43,4 @@ export type ServerHandlers = MiscHandlers &
|
||||
TagsHandlers &
|
||||
AuthHandlers;
|
||||
|
||||
export type Handlers = {} & ServerHandlers & ApiHandlers;
|
||||
|
||||
export type HandlerFunctions = Handlers[keyof Handlers];
|
||||
|
||||
27
packages/loot-core/src/types/server-handlers.ts
Normal file
27
packages/loot-core/src/types/server-handlers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { QueryState } from '../shared/query';
|
||||
|
||||
export type ServerHandlers = {
|
||||
undo: () => Promise<void>;
|
||||
redo: () => Promise<void>;
|
||||
|
||||
'make-filters-from-conditions': (arg: {
|
||||
conditions: unknown;
|
||||
applySpecialCases?: boolean;
|
||||
}) => Promise<{ filters: unknown[] }>;
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
query: (query: QueryState) => Promise<{ data: any; dependencies: string[] }>;
|
||||
|
||||
'get-server-version': () => Promise<
|
||||
{ error: 'no-server' } | { error: 'network-failure' } | { version: string }
|
||||
>;
|
||||
|
||||
'get-server-url': () => Promise<string | null>;
|
||||
|
||||
'set-server-url': (arg: {
|
||||
url: string;
|
||||
validate?: boolean;
|
||||
}) => Promise<{ error?: string }>;
|
||||
|
||||
'app-focused': () => Promise<void>;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Refactor client-server communication to use typed server proxy methods for improved clarity and safety.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Add post-merge hook to automatically install dependencies when yarn.lock changes after merges.
|
||||
Reference in New Issue
Block a user