Skip to content

Instantly share code, notes, and snippets.

@joshuabaker
Created November 4, 2024 17:04
Show Gist options
  • Save joshuabaker/afdd92ca4c19c7c10ed011c81ae651c8 to your computer and use it in GitHub Desktop.
Save joshuabaker/afdd92ca4c19c7c10ed011c81ae651c8 to your computer and use it in GitHub Desktop.
📦 Node.js FreeAgent Data Exporter

Node.js FreeAgent Data Exporter

A simple script for exporting everything from a FreeAgent account as JSON formatted data. I made this because I wanted something more machine accessible (i.e. nested structures).

Usage

  1. Download script.js and package.json, and place them into a folder together
  2. Run pnpm install, or similar
  3. Replace the ACCESS_TOKEN with your access token (see below)
  4. Run the script (i.e. node script.js or pnpm start)

Getting Access Tokens

  1. Register a new app at FreeAgent Developer Dashboard to get OAuth credentials
  2. Go to Google OAuth Playground
  3. Configure OAuth endpoints:
    • Auth: https://api.freeagent.com/v2/approve_app
    • Token: https://api.freeagent.com/v2/token_endpoint
  4. Enter your OAuth Client ID and Secret in the settings (gear icon)
  5. Type anything in scope name (e.g. admin) and click "Authorize APIs"
  6. Log in to your FreeAgent account when prompted
  7. Click "Exchange Authorization Code for Tokens"
{
"name": "freeagent-exporter",
"version": "1.0.0",
"description": "",
"main": "script.js",
"type": "module",
"scripts": {
"start": "node script.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"cli-progress": "^3.12.0",
"node-fetch": "^3.3.2"
}
}
import fetch from 'node-fetch';
import { promises as fs } from 'fs';
import path from 'path';
// Configuration
const ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN';
const BASE_URL = 'https://api.freeagent.com/v2';
// Ensure output directory exists
async function ensureOutDir() {
const outDir = path.join(process.cwd(), 'out');
await fs.mkdir(outDir, { recursive: true });
return outDir;
}
// Generic fetch all pages function
const fetchAllPages = async (endpoint, responseKey) => {
let allItems = [];
// Get first page
const firstPage = await fetch(`${BASE_URL}${endpoint}&page=1&per_page=100`, {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (!firstPage.ok) {
console.error('API Error:', {
status: firstPage.status,
statusText: firstPage.statusText
});
const errorBody = await firstPage.text();
console.error('Error response:', errorBody);
throw new Error(`API request failed: ${firstPage.status} ${firstPage.statusText}`);
}
// Get total count from header
const totalCount = parseInt(firstPage.headers.get('X-Total-Count'), 10);
if (!totalCount) {
console.error('No X-Total-Count header found');
throw new Error('Unable to determine total number of items');
}
const firstPageData = await firstPage.json();
if (!firstPageData || !firstPageData[responseKey]) {
console.error('API Response:', firstPageData);
throw new Error(`Invalid API response for ${endpoint}`);
}
allItems = [...firstPageData[responseKey]];
// Calculate total pages needed
const totalPages = Math.ceil(totalCount / 100);
// Fetch remaining pages
for (let page = 2; page <= totalPages; page++) {
console.log(`Fetching ${endpoint} page ${page}/${totalPages}...`);
const response = await fetch(`${BASE_URL}${endpoint}?page=${page}&per_page=100`, {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const items = data[responseKey];
if (!items || items.length === 0) break;
allItems = [...allItems, ...items];
}
console.log(`Completed fetching ${endpoint} - ${allItems.length} items total`);
return allItems;
}
// Fetch functions for each type
async function fetchEstimates() {
console.log('Fetching estimates...');
return fetchAllPages('/estimates?nested_estimate_items=true', 'estimates');
}
async function fetchProjects() {
console.log('Fetching projects...');
return fetchAllPages('/projects?', 'projects');
}
async function fetchInvoices() {
console.log('Fetching invoices...');
return fetchAllPages('/invoices?nested_invoice_items=true', 'invoices');
}
async function fetchContacts() {
console.log('Fetching contacts...');
return fetchAllPages('/contacts?', 'contacts');
}
// Additional fetch functions for each type
async function fetchBankAccounts() {
console.log('Fetching bank accounts...');
return fetchAllPages('/bank_accounts?', 'bank_accounts');
}
async function fetchBankTransactions() {
console.log('Fetching bank transactions...');
// First get all bank accounts
const bankAccounts = await fetchBankAccounts();
let allTransactions = [];
// Fetch transactions for each bank account
for (const account of bankAccounts) {
console.log(`Fetching transactions for bank account: ${account.url}`);
// Remove the base URL from the account.url since it's already in BASE_URL
const accountUrlPath = account.url.replace('https://api.freeagent.com/v2', '');
const accountTransactions = await fetchAllPages(
`/bank_transactions?bank_account=${encodeURIComponent(accountUrlPath)}`,
'bank_transactions'
);
allTransactions = [...allTransactions, ...accountTransactions];
}
return allTransactions;
}
async function fetchBills() {
console.log('Fetching bills...');
return fetchAllPages('/bills?nested_items=true', 'bills');
}
async function fetchCreditNotes() {
console.log('Fetching credit notes...');
return fetchAllPages('/credit_notes?nested_items=true', 'credit_notes');
}
async function fetchExpenses() {
console.log('Fetching expenses...');
return fetchAllPages('/expenses?', 'expenses');
}
async function fetchTasks() {
console.log('Fetching tasks...');
return fetchAllPages('/tasks?', 'tasks');
}
async function fetchTimeslips() {
console.log('Fetching timeslips...');
return fetchAllPages('/timeslips?', 'timeslips');
}
async function fetchUsers() {
console.log('Fetching users...');
return fetchAllPages('/users?', 'users');
}
// Save data to file
async function saveToFile(data, filename) {
const outDir = await ensureOutDir();
const filePath = path.join(outDir, filename);
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
console.log(`Saved ${filename}`);
}
// Main execution split into steps
async function main() {
try {
// Step 1: Ensure output directory exists
console.log('Setting up output directory...');
await ensureOutDir();
// Fetch all content types
const contentTypes = [
{ fetch: fetchEstimates, filename: 'estimates.json' },
{ fetch: fetchProjects, filename: 'projects.json' },
{ fetch: fetchInvoices, filename: 'invoices.json' },
{ fetch: fetchContacts, filename: 'contacts.json' },
{ fetch: fetchBankAccounts, filename: 'bank_accounts.json' },
{ fetch: fetchBankTransactions, filename: 'bank_transactions.json' },
{ fetch: fetchBills, filename: 'bills.json' },
{ fetch: fetchCreditNotes, filename: 'credit_notes.json' },
{ fetch: fetchExpenses, filename: 'expenses.json' },
{ fetch: fetchTasks, filename: 'tasks.json' },
{ fetch: fetchTimeslips, filename: 'timeslips.json' },
{ fetch: fetchUsers, filename: 'users.json' }
];
for (const { fetch, filename } of contentTypes) {
try {
const data = await fetch();
await saveToFile(data, filename);
} catch (error) {
console.error(`Failed to fetch ${filename}:`, error.message);
// Continue with next content type instead of stopping completely
}
}
console.log('Export completed successfully!');
} catch (error) {
console.error('Export failed:', error.message);
process.exit(1);
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment