Public/Migration/Import-DataverseData.ps1
|
function Import-DataverseData { <# .SYNOPSIS Imports data from a ZIP file into a Dataverse environment. .DESCRIPTION Imports data into Dataverse from a previously exported data file. Handles dependency ordering, circular references, and many-to-many relationships. This cmdlet wraps the ppds CLI tool. .PARAMETER Profile Authentication profile name. If not specified, uses the active profile. Create profiles with Connect-DataverseEnvironment or 'ppds auth create'. .PARAMETER Environment Environment URL, friendly name, unique name, or ID. Overrides the profile's default environment if specified. .PARAMETER DataPath Path to the data.zip file containing exported data. .PARAMETER BypassPlugins Bypass custom plugin execution during import. Valid values: sync, async, all Requires prvBypassCustomBusinessLogic privilege. .PARAMETER BypassFlows Bypass Power Automate flow triggers during import. .PARAMETER ContinueOnError Continue import on individual record failures. Failed records are logged but don't stop the import. .PARAMETER Mode Import mode for handling existing records: - Create: Create new records only (fails if exists) - Update: Update existing records only (fails if not exists) - Upsert: Create or update as needed (default) .PARAMETER UserMappingPath Path to user mapping XML file for remapping user references. Use New-DataverseUserMapping to generate this file. .PARAMETER StripOwnerFields Strip ownership fields (ownerid, createdby, modifiedby) allowing Dataverse to assign the current user. .PARAMETER SkipMissingColumns Skip columns that exist in exported data but not in target environment. Prevents schema mismatch errors when environments differ. .PARAMETER PassThru Return an import result object with statistics. .EXAMPLE Import-DataverseData -DataPath "./data.zip" Imports data using the active profile with default settings (Upsert mode). .EXAMPLE Import-DataverseData -Profile "prod" -DataPath "./data.zip" ` -BypassPlugins all -BypassFlows -ContinueOnError Imports with all plugins and flows bypassed, continuing on errors. .EXAMPLE Import-DataverseData -DataPath "./data.zip" -UserMappingPath "./usermapping.xml" ` -StripOwnerFields -SkipMissingColumns Imports with user remapping and flexible schema handling. .OUTPUTS None by default. PSCustomObject with import statistics if -PassThru is specified. #> [CmdletBinding()] param( [Parameter()] [string]$Profile, [Parameter()] [string]$Environment, [Parameter(Mandatory)] [string]$DataPath, [Parameter()] [ValidateSet('sync', 'async', 'all')] [string]$BypassPlugins, [Parameter()] [switch]$BypassFlows, [Parameter()] [switch]$ContinueOnError, [Parameter()] [ValidateSet('Create', 'Update', 'Upsert')] [string]$Mode = 'Upsert', [Parameter()] [string]$UserMappingPath, [Parameter()] [switch]$StripOwnerFields, [Parameter()] [switch]$SkipMissingColumns, [Parameter()] [switch]$PassThru ) # Validate data file exists if (-not (Test-Path $DataPath)) { throw "Data file not found: $DataPath" } # Validate user mapping file if specified if ($UserMappingPath -and -not (Test-Path $UserMappingPath)) { throw "User mapping file not found: $UserMappingPath" } # Get the CLI tool $cliPath = Get-PpdsCli # Build arguments $cliArgs = @( 'data', 'import' '--data', (Resolve-Path $DataPath).Path '--json' # Always use JSON for progress parsing ) if ($Profile) { $cliArgs += '--profile' $cliArgs += $Profile } if ($Environment) { $cliArgs += '--environment' $cliArgs += $Environment } if ($BypassPlugins) { $cliArgs += '--bypass-plugins' $cliArgs += $BypassPlugins } if ($BypassFlows) { $cliArgs += '--bypass-flows' } if ($ContinueOnError) { $cliArgs += '--continue-on-error' } if ($Mode -ne 'Upsert') { $cliArgs += '--mode' $cliArgs += $Mode } if ($UserMappingPath) { $cliArgs += '--user-mapping' $cliArgs += (Resolve-Path $UserMappingPath).Path } if ($StripOwnerFields) { $cliArgs += '--strip-owner-fields' } if ($SkipMissingColumns) { $cliArgs += '--skip-missing-columns' } Write-Verbose "Executing: $cliPath $($cliArgs -join ' ')" # Execute CLI and parse progress $importResult = [PSCustomObject]@{ RecordsProcessed = 0 RecordsFailed = 0 Duration = [TimeSpan]::Zero } $errorOutput = @() $currentTier = -1 & $cliPath @cliArgs 2>&1 | ForEach-Object { $line = $_ # Check if it's a JSON progress line if ($line -match '^\s*\{') { try { $progress = $line | ConvertFrom-Json switch ($progress.phase) { 'analyzing' { Write-Verbose $progress.message } 'import' { if ($null -ne $progress.tier -and $progress.tier -ne $currentTier) { $currentTier = $progress.tier Write-Verbose "Processing tier $currentTier" } if ($progress.entity -and $progress.total -gt 0) { $percent = [math]::Min(100, [math]::Round(($progress.current / $progress.total) * 100)) $status = "$($progress.current)/$($progress.total)" if ($progress.rps) { $status += " @ $([math]::Round($progress.rps, 1)) rps" } Write-Progress -Activity "Importing $($progress.entity)" ` -PercentComplete $percent ` -Status $status } elseif ($progress.message) { Write-Verbose $progress.message } } 'deferred' { if ($progress.entity -and $progress.field) { $percent = [math]::Min(100, [math]::Round(($progress.current / $progress.total) * 100)) Write-Progress -Activity "Updating deferred field: $($progress.entity).$($progress.field)" ` -PercentComplete $percent ` -Status "$($progress.current)/$($progress.total)" } } 'complete' { Write-Progress -Activity "Import complete" -Completed $importResult.RecordsProcessed = $progress.recordsProcessed $importResult.RecordsFailed = $progress.errors if ($progress.duration) { $importResult.Duration = [TimeSpan]::Parse($progress.duration) } Write-Verbose "Completed in $($progress.duration). Records: $($progress.recordsProcessed), Errors: $($progress.errors)" } 'error' { $errorOutput += $progress.message } } } catch { # Not valid JSON, treat as regular output Write-Verbose $line } } else { # Regular output if ($line -is [System.Management.Automation.ErrorRecord]) { $errorOutput += $line.ToString() } else { Write-Verbose $line } } } # Check exit code if ($LASTEXITCODE -eq 2) { # Complete failure $errorMessage = if ($errorOutput.Count -gt 0) { $errorOutput -join "`n" } else { "Import failed with exit code $LASTEXITCODE" } throw $errorMessage } elseif ($LASTEXITCODE -eq 1) { # Partial success Write-Warning "Import completed with some failures. $($importResult.RecordsFailed) records failed." } Write-Progress -Activity "Import" -Completed if ($PassThru) { return $importResult } } |