Public/Invoke-Connector.ps1
|
<# .SYNOPSIS Executes the SuccessFactors connector to retrieve and process data from multiple resource types. .DESCRIPTION Connects to SuccessFactors and retrieves data from predefined resource types including users, employees, jobs, cost centers, positions, departments, companies, locations, job functions, persons, and personal information. Processes each resource and builds connector objects for further processing. Each resource type is mapped to its corresponding unique identifier field. .PARAMETER ConnectorConfiguration The configuration object containing SuccessFactors connection details and authentication credentials. .OUTPUTS System.Object - Connector objects with external ID and processed resource data. .NOTES Retrieves the following resource types with their unique ID fields: - User (userId), EmpEmployment (employmentId), EmpJob (userId, startDate, seqNumber) - FOCostCenter, Position, FODepartment, FOCompany, FOLocation, FOJobFunction (all use externalCode) - PerPerson, PerPersonal (both use personIdExternal) Failed items are logged but do not halt processing. Future enhancement: Resource type definitions and ID field mappings should be moved to ConnectorConfiguration for tenant-specific customization. #> function ConvertTo-SFDateTimeOffset { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] [AllowNull()] $Value ) process { if ($null -eq $Value -or [string]::IsNullOrWhiteSpace([string]$Value)) { return $null } if ($Value -is [DateTimeOffset]) { return [DateTimeOffset]::new($Value.Year, $Value.Month, $Value.Day, 0, 0, 0, [TimeSpan]::Zero) } if ($Value -is [DateTime]) { return [DateTimeOffset]::new($Value.Year, $Value.Month, $Value.Day, 0, 0, 0, [TimeSpan]::Zero) } $text = [string]$Value if ($text -match '^/Date\((?<milliseconds>-?\d+)(?:[+-]\d{4})?\)/$') { $epochDate = [DateTimeOffset]::FromUnixTimeMilliseconds([long]$Matches.milliseconds) return [DateTimeOffset]::new($epochDate.Year, $epochDate.Month, $epochDate.Day, 0, 0, 0, [TimeSpan]::Zero) } $parsed = [DateTimeOffset]::MinValue if ([DateTimeOffset]::TryParse($text, [ref]$parsed)) { return [DateTimeOffset]::new($parsed.Year, $parsed.Month, $parsed.Day, 0, 0, 0, [TimeSpan]::Zero) } return $null } } function Get-SFEmploymentReferenceDate { [CmdletBinding()] param ( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Employment, [Parameter(Mandatory)] [DateTimeOffset]$AsOfDate ) $datedEmployment = foreach ($item in $Employment) { $startDate = ConvertTo-SFDateTimeOffset $item.startDate $endDate = ConvertTo-SFDateTimeOffset $item.endDate [PSCustomObject]@{ StartDate = $startDate EndDate = $endDate } } $active = @($datedEmployment | Where-Object { ($null -eq $_.StartDate -or $_.StartDate -le $AsOfDate) -and ($null -eq $_.EndDate -or $_.EndDate -ge $AsOfDate) }) if ($active.Count -gt 0) { return $AsOfDate } $latestEnded = $datedEmployment | Where-Object { $null -ne $_.EndDate -and $_.EndDate -lt $AsOfDate } | Sort-Object -Property EndDate -Descending | Select-Object -First 1 if ($null -ne $latestEnded) { return $latestEnded.EndDate } $earliestFuture = $datedEmployment | Where-Object { $null -ne $_.StartDate -and $_.StartDate -gt $AsOfDate } | Sort-Object -Property StartDate | Select-Object -First 1 if ($null -ne $earliestFuture) { return $earliestFuture.StartDate } return $AsOfDate } function Select-SFEffectiveEmpJob { [CmdletBinding()] param ( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$EmpJob, [Parameter(Mandatory)] [hashtable]$EmploymentByUserId, [Parameter(Mandatory)] [DateTimeOffset]$AsOfDate ) foreach ($userGroup in ($EmpJob | Group-Object -Property { [string]$_.userId })) { $userId = [string]$userGroup.Name $employment = @( if ($EmploymentByUserId.ContainsKey($userId)) { $EmploymentByUserId[$userId] } ) $referenceDate = Get-SFEmploymentReferenceDate -Employment $employment -AsOfDate $AsOfDate $datedJobs = foreach ($job in $userGroup.Group) { $startDate = ConvertTo-SFDateTimeOffset $job.startDate if ($null -eq $startDate) { Write-Warning "Skipping EmpJob for user '$userId' because startDate '$($job.startDate)' cannot be parsed." continue } $sequence = [long]0 [void][long]::TryParse([string]$job.seqNumber, [ref]$sequence) [PSCustomObject]@{ Job = $job StartDate = $startDate EndDate = ConvertTo-SFDateTimeOffset $job.endDate Sequence = $sequence } } $selected = $datedJobs | Where-Object { $_.StartDate -le $referenceDate -and ($null -eq $_.EndDate -or $_.EndDate -ge $referenceDate) } | Sort-Object -Property @( @{ Expression = 'StartDate'; Descending = $true }, @{ Expression = 'Sequence'; Descending = $true } ) | Select-Object -First 1 if ($null -eq $selected) { Write-Warning "No EmpJob for user '$userId' is effective on or before reference date $($referenceDate.ToString('yyyy-MM-dd')); no EmpJob object will be emitted." continue } $selected.Job } } function Invoke-Connector { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $ConnectorConfiguration ) # Connect to SuccessFactors and retrieve access token. Connect-SF -ConnectorConfiguration $ConnectorConfiguration Write-ConnectorVerbose "Connected successfully" -Verbose $utcToday = [DateTime]::UtcNow.Date $asOfDateValue = [DateTimeOffset]::new($utcToday.Year, $utcToday.Month, $utcToday.Day, 0, 0, 0, [TimeSpan]::Zero) $asOfDate = $asOfDateValue.ToString('yyyy-MM-dd') $employmentByUserId = @{} # Define the resource types to retrieve from SuccessFactors. # NOTE: Resource type definitions could be moved to ConnectorConfiguration for tenant-specific customization. $resourceTypes = @( @{Type='User'; IdField='userId'; QueryString='?$expand=personKeyNav'}, @{Type='EmpEmployment'; IdField='employmentId'}, @{Type='EmpJob'; QueryString = "?asOfDate=$asOfDate&`$expand=employeeClassNav,employmentTypeNav,emplStatusNav,jobCodeNav/jobFunctionNav"}, @{Type='FOJobCode'; IdField= 'externalCode'; QueryString= "?asOfDate=$asOfDate&`$expand=jobFunctionNav/parentFunctionCodeNav"} @{Type='FOCostCenter'; IdField='externalCode'; QueryString="?asOfDate=$asOfDate"}, @{Type='Position'; IdField='code'; QueryString="?asOfDate=$asOfDate&`$expand=parentPosition"}, @{Type='FOCompany'; IdField='externalCode'; QueryString="?asOfDate=$asOfDate"}, @{Type='FOLocation'; IdField='externalCode'; QueryString="?asOfDate=$asOfDate&`$expand=addressNavDEFLT"}, @{Type='FOJobFunction'; IdField='externalCode'; QueryString="?asOfDate=$asOfDate&`$expand=parentFunctionCodeNav"}, @{Type='PerPerson'; IdField='personIdExternal'; QueryString='?$expand=nationalIdNav'}, @{Type='PerPersonal'; IdField='personIdExternal'; QueryString="?asOfDate=$asOfDate"} ) # Retrieve and process each resource type. $resourceTypes | ForEach-Object { $resourceType = $_.Type $idField = $_.IdField if ($null -ne $_.QueryString) { $queryString = $_.QueryString } else { $queryString = $null } Write-ConnectorVerbose "Retrieving resources of type: $resourceType, id field: $idField" $resources = @(Invoke-SFRestMethod -Resource $resourceType -BaseUrl $ConnectorConfiguration.configuration.baseurl -QueryString $queryString) $sourceCount = $resources.Count if ($resourceType -eq 'EmpEmployment') { foreach ($employment in $resources) { $userId = [string]$employment.userId if (-not [string]::IsNullOrWhiteSpace($userId)) { $employmentByUserId[$userId] = @($employmentByUserId[$userId]) + $employment } } } elseif ($resourceType -eq 'EmpJob') { $resources = @(Select-SFEffectiveEmpJob -EmpJob $resources -EmploymentByUserId $employmentByUserId -AsOfDate $asOfDateValue) Write-ConnectorVerbose "Selected $($resources.Count) effective EmpJob resources from $sourceCount source rows" } $count = 0 $resources | ForEach-Object { $idValue = $null try { if ($resourceType -eq 'EmpJob') { # EmpJob has a composite OData key. $startDateKey = ([DateTimeOffset]$_.startDate).ToString('yyyy-MM-dd') $idValue = "$($_.userId)|$startDateKey|$($_.seqNumber)" } else { $idValue = $_.$idField } Build-ConnectorObject -ExternalId $idValue -Data ($_ | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashtable -Depth 10) -ObjectType $resourceType $count += 1 } catch { Write-ConnectorVerbose "Failed to process $resourceType object '$idValue': $_" } } Write-ConnectorVerbose "Processed $count resources of type $resourceType" } } |