private/Add-MsecExcelRow.ps1
|
function Add-MsecExcelRow { <# .SYNOPSIS Appends one row to a worksheet's Excel table. .DESCRIPTION Data only - charts live on the Dashboard sheet and are built once by Add-MsecExcelDashboard. That separation is the point: after a sheet exists, this does nothing but append rows, so the charts (and anything you changed about them in Excel) are never touched. The charts use ordinary cell ranges, which are pinned to the row count they were written with, so Add-MsecExcelDashboard refreshes those ranges in place after this has appended. It refreshes only the ranges - never the chart - which is why the two are separate functions. SCHEMA DRIFT IS DETECTED, NOT ABSORBED -------------------------------------- Export-Excel -Append maps values onto the EXISTING header and silently discards any property that is not already a column. That is a quiet data-loss bug waiting to happen: add a field to a measurement and it would never reach the workbook, with nothing said. So the row's columns are compared against the header first, and a mismatch takes the slow path - rewrite the sheet with the new column - with a warning saying so. Normal runs never take that path. .PARAMETER Path The .xlsx file. Created if it does not exist. .PARAMETER WorksheetName Sheet to append to. Created, with its table, if absent. .PARAMETER Row One object. Its properties become the columns on first write. .PARAMETER TableName Excel table name. Must not change once a sheet exists. .PARAMETER TableStyle An EPPlus TableStyles name - Light1-21, Medium1-28 or Dark1-11. Passed on the append path as well as on create, so changing it restyles an existing table on the next run rather than only applying to new sheets. #> [CmdletBinding()] [OutputType([int])] param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $WorksheetName, [Parameter(Mandatory)] [object] $Row, [Parameter(Mandatory)] [string] $TableName, # Typed as [string], not as [OfficeOpenXml.Table.TableStyles]. That enum only exists # once ImportExcel has been imported, and a parameter's type is resolved when the # function is DEFINED - so naming it here would break `Import-Module Msec` on any # machine without ImportExcel installed, which is every machine that never runs the # posture report. Validated by the cast in Export-MsecPostureReport instead. [string] $TableStyle = 'Medium2' ) # A collection would land in the cell as 'System.Object[]'. These do occur - # AdminsNotMfaCapableUpn and TopFailingPolicies are both arrays. $flat = [ordered]@{} foreach ($property in $Row.PSObject.Properties) { $value = $property.Value if ($null -ne $value -and $value -isnot [string] -and $value -is [System.Collections.IEnumerable]) { $value = (@($value) | ForEach-Object { "$_" }) -join '; ' } $flat[$property.Name] = $value } $flatRow = [pscustomobject] $flat $rowColumns = @($flat.Keys) # Existing header, if any. A missing file and a missing sheet both mean "create". $existingHeader = $null if (Test-Path -LiteralPath $Path) { $package = Open-ExcelPackage -Path $Path try { $worksheet = $package.Workbook.Worksheets[$WorksheetName] if ($worksheet -and $worksheet.Dimension) { $existingHeader = @( 1..$worksheet.Dimension.Columns | ForEach-Object { $worksheet.Cells[1, $_].Text } ) | Where-Object { $_ } } } finally { Close-ExcelPackage $package -NoSave } } if ($existingHeader) { # Compared as a SET, not as an ordered sequence. Export-Excel -Append maps values onto # the header BY NAME, so a row whose properties come back in a different order still # lands in the right columns - checking order too would force a needless sheet # rewrite, and a rewrite is the one thing here that discards manual formatting. # # What -Append does not survive is a column it has never seen: it drops that property # silently, which is the whole reason this wrapper exists. $same = -not (Compare-Object $existingHeader $rowColumns) if ($same) { $flatRow | Export-Excel -Path $Path -WorksheetName $WorksheetName -Append ` -TableName $TableName -TableStyle $TableStyle return (Get-MsecExcelRowCount -Path $Path -WorksheetName $WorksheetName) } $added = @($rowColumns | Where-Object { $_ -notin $existingHeader }) Write-Warning ("Sheet '$WorksheetName' has a different column set than this run produced" + $(if ($added.Count) { " (new: $($added -join ', '))" } else { '' }) + '. Rewriting the sheet to take the new shape - any manual formatting on it will be lost, though the dashboard charts are untouched. This happens only when a measurement changes shape.') return (Write-MsecExcelSheet -Path $Path -WorksheetName $WorksheetName ` -TableName $TableName -NewRow $flatRow -TableStyle $TableStyle) } # First write for this sheet. return (Write-MsecExcelSheet -Path $Path -WorksheetName $WorksheetName ` -TableName $TableName -NewRow $flatRow -TableStyle $TableStyle) } |