actual-api.ts•6.34 kB
import api from '@actual-app/api';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { BudgetFile } from './types.js';
import {
APIAccountEntity,
APICategoryEntity,
APICategoryGroupEntity,
APIPayeeEntity,
} from '@actual-app/api/@types/loot-core/src/server/api-models.js';
import { RuleEntity, TransactionEntity } from '@actual-app/api/@types/loot-core/src/types/models/index.js';
const DEFAULT_DATA_DIR: string = path.resolve(os.homedir() || '.', '.actual');
// API initialization state
let initialized = false;
let initializing = false;
let initializationError: Error | null = null;
/**
* Initialize the Actual Budget API
*/
export async function initActualApi(): Promise<void> {
if (initialized) return;
if (initializing) {
// Wait for initialization to complete if already in progress
while (initializing) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (initializationError) throw initializationError;
return;
}
try {
console.error('Initializing Actual Budget API...');
const dataDir = process.env.ACTUAL_DATA_DIR || DEFAULT_DATA_DIR;
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
await api.init({
dataDir,
serverURL: process.env.ACTUAL_SERVER_URL,
password: process.env.ACTUAL_PASSWORD,
});
const budgets: BudgetFile[] = await api.getBudgets();
if (!budgets || budgets.length === 0) {
throw new Error('No budgets found. Please create a budget in Actual first.');
}
// Use specified budget or the first one
const budgetId: string = process.env.ACTUAL_BUDGET_SYNC_ID || budgets[0].cloudFileId || budgets[0].id || '';
console.error(`Loading budget: ${budgetId}`);
await api.downloadBudget(
budgetId,
process.env.ACTUAL_BUDGET_ENCRYPTION_PASSWORD
? {
password: process.env.ACTUAL_BUDGET_ENCRYPTION_PASSWORD,
}
: undefined
);
initialized = true;
console.error('Actual Budget API initialized successfully');
} catch (error) {
console.error('Failed to initialize Actual Budget API:', error);
initializationError = error instanceof Error ? error : new Error(String(error));
throw initializationError;
} finally {
initializing = false;
}
}
/**
* Shutdown the Actual Budget API
*/
export async function shutdownActualApi(): Promise<void> {
if (!initialized) return;
await api.shutdown();
initialized = false;
}
// ----------------------------
// FETCH
// ----------------------------
/**
* Get all accounts (ensures API is initialized)
*/
export async function getAccounts(): Promise<APIAccountEntity[]> {
await initActualApi();
return api.getAccounts();
}
/**
* Get all categories (ensures API is initialized)
*/
export async function getCategories(): Promise<APICategoryEntity[]> {
await initActualApi();
return api.getCategories();
}
/**
* Get all category groups (ensures API is initialized)
*/
export async function getCategoryGroups(): Promise<APICategoryGroupEntity[]> {
await initActualApi();
return api.getCategoryGroups();
}
/**
* Get all payees (ensures API is initialized)
*/
export async function getPayees(): Promise<APIPayeeEntity[]> {
await initActualApi();
return api.getPayees();
}
/**
* Get transactions for a specific account and date range (ensures API is initialized)
*/
export async function getTransactions(accountId: string, start: string, end: string): Promise<TransactionEntity[]> {
await initActualApi();
return api.getTransactions(accountId, start, end);
}
/**
* Get all rules (ensures API is initialized)
*/
export async function getRules(): Promise<RuleEntity[]> {
await initActualApi();
return api.getRules();
}
// ----------------------------
// ACTION
// ----------------------------
/**
* Create a new payee (ensures API is initialized)
*/
export async function createPayee(args: Record<string, unknown>): Promise<string> {
await initActualApi();
return api.createPayee(args);
}
/**
* Update a payee (ensures API is initialized)
*/
export async function updatePayee(id: string, args: Record<string, unknown>): Promise<unknown> {
await initActualApi();
return api.updatePayee(id, args);
}
/**
* Delete a payee (ensures API is initialized)
*/
export async function deletePayee(id: string): Promise<unknown> {
await initActualApi();
return api.deletePayee(id);
}
/**
* Create a new rule (ensures API is initialized)
*/
export async function createRule(args: Record<string, unknown>): Promise<RuleEntity> {
await initActualApi();
return api.createRule(args);
}
/**
* Update a rule (ensures API is initialized)
*/
export async function updateRule(args: Record<string, unknown>): Promise<RuleEntity> {
await initActualApi();
return api.updateRule(args);
}
/**
* Delete a rule (ensures API is initialized)
*/
export async function deleteRule(id: string): Promise<boolean> {
await initActualApi();
return api.deleteRule(id);
}
/**
* Create a new category (ensures API is initialized)
*/
export async function createCategory(args: Record<string, unknown>): Promise<string> {
await initActualApi();
return api.createCategory(args);
}
/**
* Update a category (ensures API is initialized)
*/
export async function updateCategory(id: string, args: Record<string, unknown>): Promise<unknown> {
await initActualApi();
return api.updateCategory(id, args);
}
/**
* Delete a category (ensures API is initialized)
*/
export async function deleteCategory(id: string): Promise<{ error?: string }> {
await initActualApi();
return api.deleteCategory(id);
}
/**
* Create a new category group (ensures API is initialized)
*/
export async function createCategoryGroup(args: Record<string, unknown>): Promise<string> {
await initActualApi();
return api.createCategoryGroup(args);
}
/**
* Update a category group (ensures API is initialized)
*/
export async function updateCategoryGroup(id: string, args: Record<string, unknown>): Promise<unknown> {
await initActualApi();
return api.updateCategoryGroup(id, args);
}
/**
* Delete a category group (ensures API is initialized)
*/
export async function deleteCategoryGroup(id: string): Promise<unknown> {
await initActualApi();
return api.deleteCategoryGroup(id);
}