Private/Get-SFResourceStructure.ps1
|
<# .SYNOPSIS Helper script for introspecting SuccessFactors resource schemas and sample payloads. .DESCRIPTION This script is a helper utility intended for local analysis and troubleshooting. It connects to SuccessFactors, retrieves a small sample from each configured resource type, and writes resource-level outputs with top-level fields, flattened field paths, and sample JSON. This script is not part of the normal connector runtime flow and is not required when running Invoke-Connector in production. .PARAMETER Config Connector configuration object used by Connect-SF. .PARAMETER OutputDir Output folder where schema and sample files are written. .PARAMETER SampleSize Number of rows to fetch per resource when generating sample data. .PARAMETER SchemaDepth Maximum recursion depth used while flattening field paths. .OUTPUTS Writes files to disk and prints a summary table to the console. .EXAMPLE $cfg = New-SFConnectorConfiguration -BaseUrl 'https://api55preview.sapsf.eu' -ClientId '...' -CompanyId '...' -UserId '...' -PrivateKeyPath '~/sf-private.pem' -PrivateKeyBodyOnly ./Get-SFResourceStructure.ps1 -Config $cfg .NOTES Helper utility script only. Keep in private tooling workflows. #> param( [Parameter(Mandatory = $true)] $Config, [string]$OutputDir = "$HOME/Documents/resource-introspection", [int]$SampleSize = 3, [int]$SchemaDepth = 5 ) # ---------- Helpers ---------- <# .SYNOPSIS Helper function that determines whether a value should be treated as scalar. .DESCRIPTION Used by schema introspection to stop recursion for null, primitive, and datetime values. .NOTES Internal helper function for Get-SFResourceStructure.ps1. #> function Test-IsScalar { param([object]$Value) if ($null -eq $Value) { return $true } if ($Value -is [string]) { return $true } if ($Value -is [ValueType]) { return $true } if ($Value -is [datetime]) { return $true } return $false } <# .SYNOPSIS Helper function that recursively flattens object properties into dot-paths. .DESCRIPTION Converts nested objects and arrays into readable path strings for schema inspection, for example parentNav.externalCode or addressNavDEFLT[].city. .NOTES Internal helper function for Get-SFResourceStructure.ps1. #> function Get-PropertyPaths { param( [Parameter(Mandatory = $false)] [AllowNull()] [object]$Object, [string]$Prefix = "", [int]$Depth = 0, [int]$MaxDepth = 5 ) $paths = [System.Collections.Generic.List[string]]::new() if ($Depth -ge $MaxDepth) { if ($Prefix) { $paths.Add($Prefix) } return $paths } if (Test-IsScalar -Value $Object) { if ($Prefix) { $paths.Add($Prefix) } return $paths } if ($Object -is [System.Collections.IDictionary]) { foreach ($k in $Object.Keys) { $name = [string]$k $nextPrefix = if ($Prefix) { "$Prefix.$name" } else { $name } $child = $Object[$k] $childPaths = Get-PropertyPaths -Object $child -Prefix $nextPrefix -Depth ($Depth + 1) -MaxDepth $MaxDepth foreach ($p in $childPaths) { $paths.Add($p) } } return $paths } if (($Object -is [System.Collections.IEnumerable]) -and -not ($Object -is [string])) { $arr = @($Object) if ($arr.Count -eq 0) { if ($Prefix) { $paths.Add("$Prefix[]") } return $paths } $nextPrefix = if ($Prefix) { "$Prefix[]" } else { "[]" } $childPaths = Get-PropertyPaths -Object $arr[0] -Prefix $nextPrefix -Depth ($Depth + 1) -MaxDepth $MaxDepth foreach ($p in $childPaths) { $paths.Add($p) } return $paths } $props = $Object.PSObject.Properties | Where-Object { $_.MemberType -in @("NoteProperty", "Property") } if (-not $props) { if ($Prefix) { $paths.Add($Prefix) } return $paths } foreach ($prop in $props) { $nextPrefix = if ($Prefix) { "$Prefix.$($prop.Name)" } else { $prop.Name } $childPaths = Get-PropertyPaths -Object $prop.Value -Prefix $nextPrefix -Depth ($Depth + 1) -MaxDepth $MaxDepth foreach ($p in $childPaths) { $paths.Add($p) } } return $paths } <# .SYNOPSIS Helper function to append $top to an OData query string. .DESCRIPTION Builds a query string that limits sampled rows while preserving existing query options, for example expand statements. .NOTES Internal helper function for Get-SFResourceStructure.ps1. #> function Merge-QueryTop { param( [string]$QueryString, [int]$Top ) if ([string]::IsNullOrWhiteSpace($QueryString)) { return "?`$top=$Top" } if ($QueryString.Contains("?")) { return "$QueryString&`$top=$Top" } return "?`$top=$Top" } # ---------- Resource definitions ---------- $resourceTypes = @( @{ Type = "User"; IdField = "userId" }, @{ Type = "EmpEmployment"; IdField = "employmentId" }, @{ Type = "EmpJob"; IdField = "jobinfoId" }, @{ Type = "FOCostCenter"; IdField = "externalCode" }, @{ Type = "Position"; IdField = "externalCode" }, @{ Type = "FODepartment"; IdField = "externalCode"; QueryString = "?`$expand=parentNav,headOfUnitNav" }, @{ Type = "FOCompany"; IdField = "externalCode" }, @{ Type = "FOLocation"; IdField = "externalCode"; QueryString = "?`$expand=addressNavDEFLT" }, @{ Type = "FOJobFunction"; IdField = "externalCode"; QueryString = "?`$expand=parentFunctionCodeNav" }, @{ Type = "PerPerson"; IdField = "personIdExternal" }, @{ Type = "PerPersonal"; IdField = "personIdExternal" } ) # ---------- Run ---------- New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null Connect-SF -ConnectorConfiguration $Config $summary = foreach ($rt in $resourceTypes) { $type = $rt.Type $baseQuery = if ($rt.ContainsKey("QueryString")) { $rt.QueryString } else { "" } $query = Merge-QueryTop -QueryString $baseQuery -Top $SampleSize $sw = [System.Diagnostics.Stopwatch]::StartNew() try { $rows = @(Invoke-SFRestMethod -BaseUrl $Config.configuration.baseurl -Resource $type -QueryString $query) $sw.Stop() $resourceDir = Join-Path $OutputDir $type New-Item -ItemType Directory -Path $resourceDir -Force | Out-Null $sample = @($rows | Select-Object -First $SampleSize) $topFields = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $flatFields = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($item in $sample) { if ($null -eq $item) { continue } $props = $item.PSObject.Properties | Where-Object { $_.MemberType -in @("NoteProperty", "Property") } foreach ($p in $props) { [void]$topFields.Add($p.Name) } $paths = Get-PropertyPaths -Object $item -MaxDepth $SchemaDepth foreach ($path in $paths) { [void]$flatFields.Add($path) } } $topFieldsSorted = @($topFields) | Sort-Object $flatFieldsSorted = @($flatFields) | Sort-Object $topFieldsSorted | Set-Content -Path (Join-Path $resourceDir "fields-top-level.txt") $flatFieldsSorted | Set-Content -Path (Join-Path $resourceDir "fields-flattened.txt") ($sample | ConvertTo-Json -Depth 20) | Set-Content -Path (Join-Path $resourceDir "sample.json") [pscustomobject]@{ Resource = $type Status = "OK" Rows = $rows.Count SampleRows = $sample.Count TopFields = $topFieldsSorted.Count FlatFields = $flatFieldsSorted.Count DurationMs = $sw.ElapsedMilliseconds Error = $null } } catch { $sw.Stop() [pscustomobject]@{ Resource = $type Status = "FAIL" Rows = 0 SampleRows = 0 TopFields = 0 FlatFields = 0 DurationMs = $sw.ElapsedMilliseconds Error = $_.Exception.Message } } } $summary | Sort-Object Status, Resource | Format-Table -AutoSize $summary | Export-Csv -NoTypeInformation -Path (Join-Path $OutputDir "summary.csv") $summary | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $OutputDir "summary.json") Write-Host "" Write-Host "Done. Output folder: $OutputDir" |