Public/Export/Export-RMMObjectCsv.ps1
|
<#
Copyright (c) 2025-2026 Robert Faddes SPDX-License-Identifier: MPL-2.0 #> function Export-RMMObjectCsv { <# .SYNOPSIS Exports DattoRMM.Core objects to a flattened CSV file using named transforms. .DESCRIPTION The Export-RMMObjectCsv function accepts DattoRMM.Core objects via pipeline (or -InputObject), detects the object type automatically, applies a named transform to flatten nested properties, and writes each row directly to a CSV file for low memory usage. Built-in transforms are provided for DRMMSite, DRMMDevice, and DRMMAlert. Each type includes a 'Default' and 'Summary' transform. The -TransformName parameter supports tab completion and defaults to 'Default' if not specified. Users can define custom transforms for any DattoRMM.Core class by creating an ExportTransforms.psd1 file in $HOME/.DattoRMM.Core/. Custom transforms are merged with built-in transforms at module load. A user entry with the same class and transform name as a built-in entry will override the built-in version. For DRMMDevice exports, the -IncludeUdf and -Udf parameters control whether user-defined fields are appended to the transform output. By default, UDFs are excluded to keep exports clean. -IncludeUdf adds all UDF columns (Udf1-Udf300) for consistent schema across appends. -Udf accepts a string array to include specific UDFs (e.g. 'Udf1', 'Udf5'). Objects are written to disk individually in the process block. This streaming approach keeps memory usage constant regardless of pipeline size, making it safe for Azure Automation and large exports. .PARAMETER InputObject The DattoRMM.Core object to export. Accepts pipeline input. All objects in a single pipeline invocation must be the same type. .PARAMETER Path The file path for the CSV output. Parent directories must exist. .PARAMETER Append Appends to an existing CSV file instead of overwriting. The caller is responsible for ensuring schema compatibility when appending. .PARAMETER IncludeTimestamp Adds an 'ExportTimestamp' column with the current UTC date and time to each row. .PARAMETER IncludeUdf Includes all user-defined fields (Udf1-Udf300) in the export for consistent column schema. Only valid for DRMMDevice objects. Ignored for other types. .PARAMETER Udf Includes specific user-defined fields by name (e.g. 'Udf1', 'Udf5'). Only valid for DRMMDevice objects. Ignored for other types. .PARAMETER Force Overwrites the output file without prompting if it already exists. .EXAMPLE Get-RMMSite | Export-RMMObjectCsv -Path .\Sites.csv Exports all sites using the default transform. .EXAMPLE Get-RMMDevice | Export-RMMObjectCsv -Path .\Devices.csv -TransformName Summary Exports all devices using the Summary transform (fewer columns). .EXAMPLE Get-RMMAlert -Status All | Export-RMMObjectCsv -Path .\Alerts.csv -IncludeTimestamp Exports all alerts with a UTC timestamp column appended to each row. .EXAMPLE Get-RMMDevice | Export-RMMObjectCsv -Path .\Devices.csv -IncludeUdf Exports all devices with all UDF columns (Udf1-Udf300) appended. .EXAMPLE Get-RMMDevice | Export-RMMObjectCsv -Path .\Devices.csv -Udf 'Udf1', 'Udf5', 'Udf10' Exports all devices with specific UDF columns appended. .EXAMPLE Get-RMMSite | Export-RMMObjectCsv -Path .\Sites.csv -Append Appends site data to an existing CSV file. .INPUTS DRMMObject. Accepts any DattoRMM.Core typed object via pipeline. Built-in transforms are provided for DRMMSite, DRMMDevice, and DRMMAlert. Custom transforms can be defined for any class. .OUTPUTS None. Writes output to the specified CSV file. .NOTES Custom export transforms can be defined by creating an ExportTransforms.psd1 file in $HOME/.DattoRMM.Core/. The file uses the same format as the built-in transforms: @{ 'DRMMSite' = @{ 'MyCustomView' = @( 'Name' 'Description' @{Name = 'Devices'; Path = 'DevicesStatus.NumberOfDevices'} ) } } Simple string entries become direct property names. Hashtable entries with Name and Path keys support dot-notation for nested property access. User transforms are loaded at module import and merged with built-in transforms. Restart the module to pick up changes. .LINK https://github.com/TheShadowTek/DattoRMM.Core/blob/main/docs/commands/Export/Export-RMMObjectCsv.md .LINK Get-RMMSite .LINK Get-RMMDevice .LINK Get-RMMAlert .LINK about_DattoRMM.CoreExport #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter( Mandatory = $true, ValueFromPipeline = $true )] [DRMMObject] $InputObject, [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [switch] $Append, [Parameter()] [switch] $IncludeTimestamp, [Parameter()] [switch] $IncludeUdf, [Parameter()] [string[]] $Udf, [Parameter()] [switch] $Force ) dynamicparam { $ParamDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() # Build ValidateSet from all loaded transform names across all types if ($Script:ExportTransforms -and $Script:ExportTransforms.Count -gt 0) { $AllTransformNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($TypeKey in $Script:ExportTransforms.Keys) { foreach ($TransformKey in $Script:ExportTransforms[$TypeKey].Keys) { [void]$AllTransformNames.Add($TransformKey) } } if ($AllTransformNames.Count -gt 0) { $TransformAttribute = [System.Management.Automation.ParameterAttribute]::new() $TransformAttribute.Mandatory = $false $ValidateSetAttribute = [System.Management.Automation.ValidateSetAttribute]::new([string[]]$AllTransformNames) $AttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $AttributeCollection.Add($TransformAttribute) $AttributeCollection.Add($ValidateSetAttribute) $TransformParam = [System.Management.Automation.RuntimeDefinedParameter]::new( 'TransformName', [string], $AttributeCollection ) $TransformParam.Value = 'Default' $ParamDictionary.Add('TransformName', $TransformParam) } } return $ParamDictionary } begin { $Initialized = $false $RowCount = 0 $DetectedTypeName = $null $SelectProperties = $null $ExportParams = $null # Resolve dynamic parameter value if ($PSBoundParameters.ContainsKey('TransformName')) { $TransformName = $PSBoundParameters['TransformName'] } else { $TransformName = 'Default' } # Resolve output path early — fail fast on bad paths $ResolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) # Handle file overwrite decision before any pipeline processing if (-not $Append) { Write-Debug "Output file: $ResolvedPath" if ((Test-Path $ResolvedPath) -and -not $Force) { if (-not $PSCmdlet.ShouldProcess($ResolvedPath, "Overwrite existing file")) { $Script:ExportCancelled = $true return } } # Clear file so process block can append rows with consistent behaviour if (Test-Path $ResolvedPath) { Remove-Item -Path $ResolvedPath -Force Write-Debug "Cleared existing file: $ResolvedPath" } } $Script:ExportCancelled = $false } process { if ($Script:ExportCancelled) {return} $TypeName = $InputObject.GetType().Name # First-object initialization: detect type, validate transform, build properties if (-not $Initialized) { $DetectedTypeName = $TypeName Write-Verbose "Detected object type: $DetectedTypeName" # Validate transform exists for the detected type if (-not $Script:ExportTransforms.ContainsKey($DetectedTypeName)) { Write-Error "No export transforms are defined for type '$DetectedTypeName'. Define a custom transform in $HOME/.DattoRMM.Core/ExportTransforms.psd1." -ErrorAction Stop return } $TypeTransforms = $Script:ExportTransforms[$DetectedTypeName] if (-not $TypeTransforms.ContainsKey($TransformName)) { $AvailableNames = ($TypeTransforms.Keys | Sort-Object) -join ', ' Write-Error "Transform '$TransformName' is not defined for type '$DetectedTypeName'. Available transforms: $AvailableNames" -ErrorAction Stop return } Write-Verbose "Using transform '$TransformName' for type '$DetectedTypeName'" # Convert transform entries to Select-Object properties $TransformEntries = $TypeTransforms[$TransformName] $SelectProperties = ConvertTo-ExportProperty -TransformEntries $TransformEntries # Handle UDF parameters for DRMMDevice if ($DetectedTypeName -eq 'DRMMDevice') { if ($IncludeUdf) { Write-Verbose "Including all UDF columns (Udf1-Udf$([DRMMDeviceUdfs]::MaxUdfCount))" for ($i = 1; $i -le [DRMMDeviceUdfs]::MaxUdfCount; $i++) { $UdfName = "Udf$i" $SelectProperties += @{ Name = $UdfName Expression = [scriptblock]::Create("`$_.Udfs.$UdfName") } } } elseif ($Udf) { Write-Verbose "Including specified UDFs: $($Udf -join ', ')" foreach ($UdfName in $Udf) { # Validate UDF name format and range if ($UdfName -notmatch '^Udf(\d{1,3})$') { Write-Warning "Skipping invalid UDF name '$UdfName'. Expected format: Udf1, Udf2, ... Udf$([DRMMDeviceUdfs]::MaxUdfCount)." continue } $UdfNum = [int]$Matches[1] if ($UdfNum -lt 1 -or $UdfNum -gt [DRMMDeviceUdfs]::MaxUdfCount) { Write-Warning "Skipping out-of-range UDF '$UdfName'. Valid range: Udf1-Udf$([DRMMDeviceUdfs]::MaxUdfCount)." continue } $SelectProperties += @{ Name = $UdfName Expression = [scriptblock]::Create("`$_.Udfs.$UdfName") } } } } elseif ($IncludeUdf -or $Udf) { Write-Warning "-IncludeUdf and -Udf parameters are only applicable to DRMMDevice objects. Ignoring." } # Add timestamp column if requested if ($IncludeTimestamp) { $SelectProperties += @{ Name = 'ExportTimestamp' Expression = {(Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')} } Write-Verbose "ExportTimestamp column enabled" } # Build Export-Csv parameters — always append since begin block handles file creation $ExportParams = @{ Path = $ResolvedPath NoTypeInformation = $true Encoding = 'UTF8' Append = $true } $Initialized = $true Write-Verbose "Exporting $DetectedTypeName objects to $ResolvedPath" } elseif ($TypeName -ne $DetectedTypeName) { Write-Error "Mixed object types are not supported. Expected '$DetectedTypeName' but received '$TypeName'." -ErrorAction Stop return } # Stream each object directly to file $InputObject | Select-Object $SelectProperties | Export-Csv @ExportParams $RowCount++ } end { if ($Script:ExportCancelled) {return} if ($RowCount -eq 0) { Write-Verbose "No objects received; nothing to export." } else { Write-Verbose "Export complete: $RowCount rows written to $ResolvedPath" } } } # SIG # Begin signature block # MIIF+wYJKoZIhvcNAQcCoIIF7DCCBegCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCACEwayvXHSt/3Z # 4vBH5dVbZ/S3POX5HoRwwKUMq2UtnqCCA04wggNKMIICMqADAgECAhB464iXHfI6 # gksEkDDTyrNsMA0GCSqGSIb3DQEBCwUAMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRk # ZXMxIzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nMB4XDTI2MDMz # MTAwMTMzMFoXDTI4MDMzMTAwMjMzMFowPTEWMBQGA1UECgwNUm9iZXJ0IEZhZGRl # czEjMCEGA1UEAwwaRGF0dG9STU0uQ29yZSBDb2RlIFNpZ25pbmcwggEiMA0GCSqG # SIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn1EpMYQgl1RgWzQj2+wp2mvdfb3UsaBS # nxEVGoQ0gj96tJ2MHAF7zsITdUjwaflKS1vE6wAlOg5EI1V79tJCMxzM0bFpOdR1 # L5F2HE/ovIAKNkHxFUF5qWU8vVeAsOViFQ4yhHpzLen0WLF6vhmc9eH23dLQy5fy # tELZQEc2WbQFa4HMAitP/P9kHAu6CUx5s4woLIOyyR06jkr3l9vk0sxcbCxx7+dF # RrsSLyPYPH+bUAB8+a0hs+6qCeteBuUfLvGzpMhpzKAsY82WZ3Rd9X38i32dYj+y # dYx+nx+UEMDLjDJrZgnVa8as4RojqVLcEns5yb/XTjLxDc58VatdAgMBAAGjRjBE # MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU # H+B0vf97dYXqdUX1YMcWhFsY6fcwDQYJKoZIhvcNAQELBQADggEBAJmD4EEGNmcD # 1JtFoRGxuLJaTHxDwBsjqcRQRE1VPZNGaiwIm8oSQdHVjQg0oIyK7SEb02cs6n6Y # NZbwf7B7WZJ4aKYbcoLug1k1x9SoqwBmfElECeJTKXf6dkRRNmrAodpGCixR4wMH # KXqwqP5F+5j7bdnQPiIVXuMesxc4tktz362ysph1bqKjDQSCBpwi0glEIH7bv5Ms # Ey9Gl3fe+vYC5W06d2LYVebEfm9+7766hsOgpdDVgdtnN+e6uwIJjG/6PTG6TMDP # y+pr5K6LyUVYJYcWWUTZRBqqwBHiLGekPbxrjEVfxUY32Pq4QfLzUH5hhUCAk4HN # XpF9pOzFLMUxggIDMIIB/wIBATBRMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRkZXMx # IzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nAhB464iXHfI6gksE # kDDTyrNsMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC # gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG # CisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIPrBYkSH0j1pc4PboPiktzVqflmB # qoGnMb+ES6t3W10uMA0GCSqGSIb3DQEBAQUABIIBAIAPZQAx8dIgonpoN2EiYjM7 # R6EXgsUMbzrjNYX2YBdke/kt9WJ2g0XRLJWTSUyo1u7p2LF4jWYJTk6iTRUjHBYo # Q2o3qoM1I+vGkU3PFDxUs98OOTbs/4/w3qfPR5b0HtvOejBLoT96lh9+VHCKDo3e # PH5D5IOk/w1gkpHtXeCHFR98v271iBwGB9RxL/HvI2EsuG+W1Hj1tT5NOUKXtA88 # eeKBqhKN/RILCjfB6oG5fTsl3OeaCt52XKudGcX+yYNo2iZOiXIz4xZuZcEUGhlw # YEGtqd7AptOYvLFzadLlqvj8Tf81lx6lSFXdh7sOcEzrUYKomajVUE7kPshsoao= # SIG # End signature block |