das-budget-sdk
Version:
An UNOFFICIAL TypeScript SDK for interacting with the DAS Budget API
316 lines (315 loc) • 12.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = __importDefault(require("axios"));
const crypto_1 = __importDefault(require("crypto"));
const types_1 = require("./types");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require('../package.json');
class DasBudget {
constructor(config) {
this.accessToken = null;
this.tokenExpiry = null;
this.userId = null;
this.budgetId = null;
this.baseUrl = 'https://api.dasbudget.com';
this.refreshToken = config.refreshToken;
this.apiKey = config.apiKey;
this.debug = config.debug || false;
}
log(message) {
if (this.debug) {
console.log(`[DasBudget SDK] ${message}`);
}
}
async refreshAccessToken() {
try {
this.log('Refreshing access token...');
const response = await axios_1.default.post(`https://securetoken.googleapis.com/v1/token?key=${this.apiKey}`, {
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
});
this.accessToken = response.data.access_token;
this.tokenExpiry = Date.now() + response.data.expires_in * 1000;
this.userId = response.data.user_id;
this.log('Access token refreshed successfully');
}
catch (error) {
this.log('Error refreshing access token');
throw error;
}
}
async ensureValidToken() {
const FIVE_MINUTES = 300000; // Get new token if within 5 minutes of expiry
if (!this.accessToken ||
!this.tokenExpiry ||
Date.now() >= this.tokenExpiry - FIVE_MINUTES) {
await this.refreshAccessToken();
}
}
/**
* Sets the budget ID to use for all future API calls.
* If not set, the oldest budget will be used by default.
* @param budgetId The ID of the budget to use
*/
setBudgetId(budgetId) {
this.budgetId = budgetId;
this.log(`Set budget ID to: ${budgetId ?? 'null'}`);
}
getHeaders(options) {
return {
Authorization: `Bearer ${this.accessToken}`,
Accept: '*/*',
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Origin: 'https://app.dasbudget.com',
Referer: 'https://app.dasbudget.com/',
'X-Das-Context-Id': options?.budgetId ?? this.budgetId ?? 'null',
'X-Das-Platform': 'web',
'X-Das-Build': '179',
'X-Das-Version': '0.9.5',
'User-Agent': `klinquist/das-budget-sdk/${version}`,
};
}
async initialize() {
this.log('Initializing SDK...');
await this.refreshAccessToken();
this.log('SDK initialized successfully');
}
async transactions(options) {
await this.ensureValidToken();
this.log('Fetching transactions...');
const since = options?.since;
if (since !== undefined) {
// Validate the since parameter
if (typeof since !== 'number' || isNaN(since)) {
throw new Error('since parameter must be a valid number (seconds since epoch)');
}
// Log the since value in different formats for debugging
this.log(`since parameter value: ${since}`);
this.log(`since as milliseconds: ${since * 1000}`);
this.log(`since as date: ${new Date(since * 1000).toISOString()}`);
}
const allTransactions = [];
let currentPage = 1;
const limit = 40; // Using the API's default limit
let hasMorePages = true;
let totalFetched = 0;
while (hasMorePages) {
try {
this.log(`Fetching page ${currentPage} with limit ${limit}...`);
const response = await axios_1.default.get(`${this.baseUrl}/api/transaction`, {
params: {
page: currentPage,
limit,
types: 'checking,credit card',
},
headers: this.getHeaders({
budgetId: options?.budgetId,
}),
});
const { transactions, total } = response.data;
this.log(`API Response - Page: ${currentPage}, Total: ${total}, Fetched: ${transactions.length}`);
this.log(`Fetched ${transactions.length} transactions from page ${currentPage}`);
// If we have a since parameter, filter transactions
if (since !== undefined) {
const filteredTransactions = transactions.filter((tx) => {
const createdAt = new Date(tx.created_at).getTime() / 1000;
this.log(`Transaction ${tx.id} created at: ${new Date(tx.created_at).toISOString()} (${createdAt})`);
this.log(`Comparing: ${createdAt} >= ${since} = ${createdAt >= since}`);
return createdAt >= since;
});
this.log(`Found ${filteredTransactions.length} transactions after ${new Date(since * 1000).toISOString()}`);
allTransactions.push(...filteredTransactions);
totalFetched += filteredTransactions.length;
// Only stop pagination if we got no transactions in this page
// or if we got fewer transactions than the limit and none of them match our filter
if (transactions.length === 0 ||
(transactions.length < limit &&
filteredTransactions.length === 0)) {
this.log('Reached end of transactions - no more matching transactions found');
hasMorePages = false;
}
else {
currentPage++;
this.log(`Moving to page ${currentPage} - found ${filteredTransactions.length} matching transactions`);
}
}
else {
// If no since parameter, just return the first page
this.log('No since parameter provided, returning first page only');
return transactions;
}
}
catch (error) {
this.log('Error fetching transactions');
throw error;
}
}
this.log(`Returning ${allTransactions.length} total transactions (${totalFetched} fetched across ${currentPage - 1} pages)`);
return allTransactions;
}
async getBucketsByKind(kind, options) {
await this.ensureValidToken();
this.log(`Fetching ${kind}s...`);
try {
const response = await axios_1.default.get(`${this.baseUrl}/api/bucket`, {
params: {
page: 1,
limit: 1000,
kind,
sort: 'schedule_date,name_clean',
},
headers: this.getHeaders(options),
});
return response.data.items;
}
catch (error) {
this.log(`Error fetching ${kind}s`);
throw error;
}
}
async expenses(options) {
return this.getBucketsByKind('expense', options);
}
async goals(options) {
return this.getBucketsByKind('goal', options);
}
async vaults(options) {
return this.getBucketsByKind('vault', options);
}
async accounts(options) {
await this.ensureValidToken();
this.log('Fetching accounts...');
try {
const response = await axios_1.default.get(`${this.baseUrl}/api/item/account`, {
params: {
types: 'checking,credit card',
},
headers: this.getHeaders(options),
});
return response.data.items;
}
catch (error) {
this.log('Error fetching accounts');
throw error;
}
}
async assignTransactionToBucket(options) {
await this.ensureValidToken();
this.log(`Assigning transaction ${options.transactionId} to bucket ${options.bucketId}...`);
try {
const actualBucketId = options.bucketId === types_1.FREE_TO_SPEND ? 'fts' : options.bucketId;
const response = await axios_1.default.post(`${this.baseUrl}/api/item/swap/${options.transactionId}/${actualBucketId}`, {}, {
headers: this.getHeaders({ budgetId: options.budgetId }),
});
return response.data;
}
catch (error) {
this.log('Error assigning transaction to bucket');
throw error;
}
}
async refreshes(options) {
await this.ensureValidToken();
this.log('Fetching refresh information...');
try {
const response = await axios_1.default.get(`${this.baseUrl}/api/item/refreshes`, {
headers: this.getHeaders(options),
});
return response.data;
}
catch (error) {
this.log('Error fetching refresh information');
throw error;
}
}
/**
* Refreshes the data for a specific account.
*
* @param options - The refresh options
* @param options.itemId - The ID of the item to refresh (required)
* @param options.usePremium - Whether to use premium refresh credits (optional, defaults to false)
* @param options.budgetId - The ID of the budget to use (optional, defaults to the currently set budget)
*
* @throws {Error} If accountId is not provided
* @throws {Error} If the account refresh fails
*
* @example
* ```typescript
* // Basic usage
* await dasBudget.refresh({ itemId: "account-123" });
*
* // Using premium refresh
* await dasBudget.refresh({
* itemId: "item-123",
* usePremium: true,
* budgetId: "budget-456"
* });
* ```
*/
async refresh(options) {
if (!options?.itemId) {
throw new Error('itemId is required for refresh');
}
if (typeof options.itemId !== 'string' ||
options.itemId.trim() === '') {
throw new Error('itemId must be a non-empty string');
}
if (options.usePremium !== undefined &&
typeof options.usePremium !== 'boolean') {
throw new Error('usePremium must be a boolean if provided');
}
if (options.budgetId !== undefined &&
(typeof options.budgetId !== 'string' ||
options.budgetId.trim() === '')) {
throw new Error('budgetId must be a non-empty string if provided');
}
await this.ensureValidToken();
this.log(`Refreshing item ${options.itemId}...`);
try {
await axios_1.default.post(`${this.baseUrl}/api/item/${options.itemId}/refresh`, {
use_premium: options.usePremium ?? false,
idempotency_key: crypto_1.default.randomUUID(),
user_initiated: true,
}, {
headers: this.getHeaders(),
});
}
catch (error) {
this.log('Error refreshing account');
throw error;
}
}
async budgets() {
await this.ensureValidToken();
this.log('Fetching budgets...');
try {
const response = await axios_1.default.get(`${this.baseUrl}/api/context`, {
headers: this.getHeaders(),
});
return response.data.items;
}
catch (error) {
this.log('Error fetching budgets');
throw error;
}
}
async items(options) {
await this.ensureValidToken();
this.log('Fetching items...');
try {
const response = await axios_1.default.get(`${this.baseUrl}/api/item`, {
headers: this.getHeaders(options),
});
return response.data.items;
}
catch (error) {
this.log('Error fetching items');
throw error;
}
}
}
exports.default = DasBudget;