Files
BC-bak/bc-export.ps1

327 lines
9.7 KiB
PowerShell
Raw Normal View History

2026-02-09 18:57:39 +01:00
#!/usr/bin/env pwsh
#
# Business Central Data Export via BC API v2.0
# Authenticates to Azure AD and extracts critical business data as JSON
2026-02-09 18:57:39 +01:00
#
param(
[Parameter(Mandatory=$true)]
[string]$OutputPath
)
# Get configuration from environment variables
$tenantId = $env:AZURE_TENANT_ID
$clientId = $env:AZURE_CLIENT_ID
$clientSecret = $env:AZURE_CLIENT_SECRET
$environmentName = $env:BC_ENVIRONMENT_NAME
$bcCompanyName = $env:BC_COMPANY_NAME # optional: filter to specific company
$baseUrl = "https://api.businesscentral.dynamics.com/v2.0/$tenantId/$environmentName/api/v2.0"
# Standalone entities to extract
$entities = @(
"accounts",
"customers",
"vendors",
"items",
"generalLedgerEntries",
"bankAccounts",
"employees",
"dimensions",
"dimensionValues",
"currencies",
"paymentTerms",
"paymentMethods",
"journals",
"countriesRegions"
)
2026-02-09 18:57:39 +01:00
# Document entities with line items - fetched with $expand to include lines
# Lines cannot be queried standalone; they require a parent document ID
$documentEntities = @{
"salesInvoices" = "salesInvoiceLines"
"salesOrders" = "salesOrderLines"
"salesCreditMemos" = "salesCreditMemoLines"
"purchaseInvoices" = "purchaseInvoiceLines"
"purchaseOrders" = "purchaseOrderLines"
}
2026-02-09 18:57:39 +01:00
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$timestamp] [$Level] $Message"
}
function Get-AzureADToken {
param(
[string]$TenantId,
[string]$ClientId,
[string]$ClientSecret
)
Write-Log "Authenticating to Azure AD..."
$tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$body = @{
client_id = $ClientId
client_secret = $ClientSecret
scope = "https://api.businesscentral.dynamics.com/.default"
grant_type = "client_credentials"
}
try {
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
Write-Log "Successfully authenticated to Azure AD"
return $response.access_token
}
catch {
Write-Log "Failed to authenticate: $_" "ERROR"
throw
}
}
function Get-BCData {
2026-02-09 18:57:39 +01:00
param(
[string]$Token,
[string]$Url
2026-02-09 18:57:39 +01:00
)
$headers = @{
"Authorization" = "Bearer $Token"
"Accept" = "application/json"
}
$allRecords = @()
$currentUrl = $Url
while ($currentUrl) {
try {
$response = Invoke-RestMethod -Uri $currentUrl -Method Get -Headers $headers
}
catch {
Write-Log "API request failed for $currentUrl : $_" "ERROR"
throw
}
if ($response.value) {
$allRecords += $response.value
}
2026-02-09 18:57:39 +01:00
# Handle OData pagination
$currentUrl = $response.'@odata.nextLink'
2026-02-09 18:57:39 +01:00
}
return $allRecords
2026-02-09 18:57:39 +01:00
}
function Get-Companies {
param([string]$Token)
Write-Log "Fetching companies..."
$companiesUrl = "$baseUrl/companies"
$companies = Get-BCData -Token $Token -Url $companiesUrl
Write-Log "Found $($companies.Count) company/companies"
return $companies
2026-02-09 18:57:39 +01:00
}
function Export-EntityData {
2026-02-09 18:57:39 +01:00
param(
[string]$Token,
[string]$CompanyId,
[string]$CompanyName,
[string]$EntityName,
[string]$OutputDir
2026-02-09 18:57:39 +01:00
)
$entityUrl = "$baseUrl/companies($CompanyId)/$EntityName"
2026-02-09 18:57:39 +01:00
Write-Log " Exporting $EntityName..."
2026-02-09 18:57:39 +01:00
try {
$data = Get-BCData -Token $Token -Url $entityUrl
$count = 0
if ($data) { $count = $data.Count }
2026-02-09 18:57:39 +01:00
$outputFile = Join-Path $OutputDir "$EntityName.json"
$data | ConvertTo-Json -Depth 10 | Out-File -FilePath $outputFile -Encoding utf8
2026-02-09 18:57:39 +01:00
Write-Log " $EntityName : $count records"
return $count
2026-02-09 18:57:39 +01:00
}
catch {
Write-Log " Failed to export ${EntityName}: $_" "WARN"
# Write empty array so downstream knows it was attempted
$outputFile = Join-Path $OutputDir "$EntityName.json"
"[]" | Out-File -FilePath $outputFile -Encoding utf8
return 0
2026-02-09 18:57:39 +01:00
}
}
function Export-DocumentWithLines {
param(
[string]$Token,
[string]$CompanyId,
[string]$CompanyName,
[string]$DocumentEntity,
[string]$LineEntity,
[string]$OutputDir
)
$entityUrl = "$baseUrl/companies($CompanyId)/$DocumentEntity" + '?$expand=' + $LineEntity
Write-Log " Exporting $DocumentEntity (with $LineEntity)..."
try {
$data = Get-BCData -Token $Token -Url $entityUrl
$docCount = 0
$lineCount = 0
if ($data) {
$docCount = $data.Count
foreach ($doc in $data) {
if ($doc.$LineEntity) {
$lineCount += $doc.$LineEntity.Count
}
}
}
# Save the documents (with lines embedded)
$outputFile = Join-Path $OutputDir "$DocumentEntity.json"
$data | ConvertTo-Json -Depth 10 | Out-File -FilePath $outputFile -Encoding utf8
Write-Log " $DocumentEntity : $docCount documents, $lineCount lines"
return ($docCount + $lineCount)
}
catch {
Write-Log " Failed to export ${DocumentEntity} with lines: $_" "WARN"
$outputFile = Join-Path $OutputDir "$DocumentEntity.json"
"[]" | Out-File -FilePath $outputFile -Encoding utf8
return 0
}
}
2026-02-09 18:57:39 +01:00
# Main execution
try {
Write-Log "========================================="
Write-Log "BC Data Export Script (API v2.0)"
2026-02-09 18:57:39 +01:00
Write-Log "========================================="
Write-Log "Environment: $environmentName"
Write-Log "Output Path: $OutputPath"
Write-Log "Entities to extract: $($entities.Count + $documentEntities.Count) ($($documentEntities.Count) with line items)"
2026-02-09 18:57:39 +01:00
# Create output directory
$exportDir = $OutputPath
if (-not (Test-Path $exportDir)) {
New-Item -ItemType Directory -Path $exportDir -Force | Out-Null
}
2026-02-09 18:57:39 +01:00
# Step 1: Get Azure AD token
$token = Get-AzureADToken -TenantId $tenantId -ClientId $clientId -ClientSecret $clientSecret
# Step 2: Get companies
$companies = Get-Companies -Token $token
2026-02-09 18:57:39 +01:00
if ($companies.Count -eq 0) {
Write-Log "No companies found in environment $environmentName" "ERROR"
exit 1
2026-02-09 18:57:39 +01:00
}
# Save companies list
$companies | ConvertTo-Json -Depth 10 | Out-File -FilePath (Join-Path $exportDir "companies.json") -Encoding utf8
# Filter to specific company if configured
$targetCompanies = $companies
if ($bcCompanyName) {
$targetCompanies = $companies | Where-Object { $_.name -eq $bcCompanyName -or $_.displayName -eq $bcCompanyName }
if ($targetCompanies.Count -eq 0) {
Write-Log "Company '$bcCompanyName' not found. Available: $($companies.name -join ', ')" "ERROR"
exit 1
}
Write-Log "Filtering to company: $bcCompanyName"
2026-02-09 18:57:39 +01:00
}
$totalRecords = 0
$totalEntities = 0
$failedEntities = @()
2026-02-09 18:57:39 +01:00
# Step 3: Export data for each company
foreach ($company in $targetCompanies) {
$companyName = $company.name
$companyId = $company.id
Write-Log "-----------------------------------------"
Write-Log "Exporting company: $companyName ($companyId)"
# Create company directory (sanitize name for filesystem)
$safeName = $companyName -replace '[\\/:*?"<>|]', '_'
$companyDir = Join-Path $exportDir $safeName
if (-not (Test-Path $companyDir)) {
New-Item -ItemType Directory -Path $companyDir -Force | Out-Null
}
# Export standalone entities
foreach ($entity in $entities) {
$count = Export-EntityData `
-Token $token `
-CompanyId $companyId `
-CompanyName $companyName `
-EntityName $entity `
-OutputDir $companyDir
$totalRecords += $count
$totalEntities++
if ($count -eq 0) {
$failedEntities += "$companyName/$entity"
}
}
# Export document entities with their line items via $expand
foreach ($docEntity in $documentEntities.Keys) {
$lineEntity = $documentEntities[$docEntity]
$count = Export-DocumentWithLines `
-Token $token `
-CompanyId $companyId `
-CompanyName $companyName `
-DocumentEntity $docEntity `
-LineEntity $lineEntity `
-OutputDir $companyDir
$totalRecords += $count
$totalEntities++
if ($count -eq 0) {
$failedEntities += "$companyName/$docEntity"
}
}
2026-02-09 18:57:39 +01:00
}
# Save export metadata
$metadata = @{
exportDate = (Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC)
environment = $environmentName
companies = @($targetCompanies | ForEach-Object { $_.name })
entitiesExported = $totalEntities
totalRecords = $totalRecords
failedEntities = $failedEntities
}
$metadata | ConvertTo-Json -Depth 5 | Out-File -FilePath (Join-Path $exportDir "export-metadata.json") -Encoding utf8
2026-02-09 18:57:39 +01:00
Write-Log "========================================="
Write-Log "Export completed"
Write-Log "Companies: $($targetCompanies.Count)"
Write-Log "Entities: $totalEntities"
Write-Log "Total records: $totalRecords"
if ($failedEntities.Count -gt 0) {
Write-Log "Failed/empty: $($failedEntities.Count) entities" "WARN"
}
2026-02-09 18:57:39 +01:00
Write-Log "========================================="
exit 0
}
catch {
Write-Log "Unexpected error: $_" "ERROR"
Write-Log "Stack trace: $($_.ScriptStackTrace)" "ERROR"
exit 1
}