|
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(); |