PSPersonio.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSPersonio.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName PSPersonio.Import.DoDotSource -Fallback $false if ($PSPersonio_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName PSPersonio.Import.IndividualFiles -Fallback $false if ($PSPersonio_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { . Import-ModuleFile -Path $path } # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { . Import-ModuleFile -Path $path } # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PSPersonio' -Language 'en-US' function ConvertTo-CamelCaseString { <# .SYNOPSIS ConvertTo-CamelCaseString .DESCRIPTION Convert a series of strings to Uppercase first strings and concatinate together as a final 'camelcased' string .PARAMETER InputObject String(s) to convert .EXAMPLE PS C:\> ConvertTo-CamelCaseString "my", "foo", "string" Return "MyFooString" #> [CmdletBinding( PositionalBinding=$true, SupportsShouldProcess=$false, ConfirmImpact="Low" )] [OutputType([string])] param ( [Parameter( Mandatory=$true, ValueFromPipeline=$true )] [string[]] $InputObject ) begin { $collection = [System.Collections.ArrayList]@() } process { foreach ($string in $InputObject) { $firstPart = $string.Substring(0,1).ToUpper() if($string.Length -gt 1) { $secondPart = $string.Substring(1, ($string.Length-1)).ToLower() } $null = $collection.Add( "$($firstPart)$($secondPart)" ) } } end { [String]::Join('', ($collection | ForEach-Object { $_ })) } } function Expand-MemberNamesFromBasicObject { <# .SYNOPSIS Expand-MemberNamesFromBasicObject .DESCRIPTION Retrieve properties names retrieved from Personio API from TypeData definition .PARAMETER TypeName Name of the type to retrieve properties from .EXAMPLE PS C:\> Expand-MemberNamesFromBasicObject -TypeNameBasic "Personio.Employee.BasicEmployee" Output properties names retrieved from Personio API from TypeData definition #> [CmdletBinding( PositionalBinding=$true, ConfirmImpact="Low" )] param ( [Parameter( Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true )] [string] $TypeName ) begin { } process { # Get TypeData from PS type system $members = Get-TypeData -TypeName "$TypeName" | Select-Object -ExpandProperty Members # work trough members of type foreach ($key in $members.Keys) { # extract scriptblock from module types.ps1xml foreach($text in ($members.$key.GetScriptBlock)) { # match property names from Baseobject if($text -match "\`$this.BaseObject.(?'attrib'\S*[^)}\]])") { # remove subproperties like "email.value" -> so only "email" will be outputted $output = $Matches.attrib.Split(".")[0] # return result $output } } } } end { } } function ConvertFrom-Base64StringWithNoPadding( [string]$Data ) { <# .SYNOPSIS Helper function build valid Base64 strings from JWT access tokens .DESCRIPTION Helper function build valid Base64 strings from JWT access tokens .PARAMETER Data The Token to convert .EXAMPLE PS C:\> ConvertFrom-Base64StringWithNoPadding -Data $data build valid base64 string the content from variable $data #> $Data = $Data.Replace('-', '+').Replace('_', '/') switch ($Data.Length % 4) { 0 { break } 2 { $Data += '==' } 3 { $Data += '=' } default { throw New-Object ArgumentException('data') } } [System.Convert]::FromBase64String($Data) } function ConvertFrom-JWTtoken { <# .SYNOPSIS Converts access tokens to readable objects .DESCRIPTION Converts access tokens to readable objects .PARAMETER Token The Token to convert .EXAMPLE PS C:\> ConvertFrom-JWTtoken -Token $Token Converts the content from variable $token to an object #> [cmdletbinding()] param( [Parameter(Mandatory = $true)] [string] $Token ) # Validate as per https://tools.ietf.org/html/rfc7519 - Access and ID tokens are fine, Refresh tokens will not work if ((-not $Token.Contains(".")) -or (-not $Token.StartsWith("eyJ"))) { $msg = "Invalid data or not an access token. $($Token)" Stop-PSFFunction -Message $msg -Tag "JWT" -EnableException $true -Exception ([System.Management.Automation.RuntimeException]::new($msg)) } # Split the token in its parts $tokenParts = $Token.Split(".") # Work on header $tokenHeader = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[0]) ) $tokenHeaderJSON = $tokenHeader | ConvertFrom-Json # Work on payload $tokenPayload = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[1]) ) $tokenPayloadJSON = $tokenPayload | ConvertFrom-Json # Work on signature $tokenSignature = ConvertFrom-Base64StringWithNoPadding $tokenParts[2] # Output $resultObject = [PSCustomObject]@{ "Header" = $tokenHeader "Payload" = $tokenPayload "Signature" = $tokenSignature "Algorithm" = $tokenHeaderJSON.alg "Type" = $tokenHeaderJSON.typ "JwtId" = [guid]::Parse($tokenPayloadJSON.jti) "Issuer" = $tokenPayloadJSON.iss "Scope" = $tokenPayloadJSON.scope "IssuedUTC" = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.iat).ToUniversalTime() "ExpiresUTC" = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.exp).ToUniversalTime() "NotBeforeUTC" = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.nbf).ToUniversalTime() "ClientId" = $tokenPayloadJSON.sub "Prv" = $tokenPayloadJSON.prv } #$output $resultObject } function Format-ApiPath { <# .Synopsis Format-ApiPath .DESCRIPTION Ensure the right format of api path uri .PARAMETER Path Path to format .PARAMETER QueryParameter A hashtable for all the parameters to the api route .PARAMETER Token AccessToken object for Personio service .EXAMPLE Format-ApiPath -Path $ApiPath Api path data from variable $ApiPath will be tested and formatted. .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( SupportsShouldProcess = $false, ConfirmImpact = 'Low' )] Param( [Parameter(Mandatory = $true)] [string] $Path, [Personio.Core.AccessToken] $Token, [hashtable] $QueryParameter ) if (-not $Token) { $Token = $script:PersonioToken } Write-PSFMessage -Level System -Message "Formatting API path '$($Path)'" # Remove no more need slashes $apiPath = $Path.Trim('/') # check on API path prefix if (-not $ApiPath.StartsWith($token.ApiUri)) { $apiPath = $token.ApiUri.Trim('/') + "/" + $apiPath Write-PSFMessage -Level System -Message "Add API prefix, finished formatting path to '$($apiPath)'" } else { Write-PSFMessage -Level System -Message "Prefix API path already present, finished formatting" } # If specified, process hashtable QueryParameters to valid parameters into uri if ($QueryParameter) { $apiPath = "$($apiPath)?" $i = 0 foreach ($key in $QueryParameter.Keys) { if ($i -gt 0) { $apiPath = "$($apiPath)&" } if ("System.Array" -in ($QueryParameter[$Key]).psobject.TypeNames) { $parts = $QueryParameter[$Key] | ForEach-Object { "$($key)=$($_)" } $apiPath = "$($apiPath)$([string]::Join("&", $parts))" } else { $apiPath = "$($apiPath)$($key)=$($QueryParameter[$Key])" } $i++ } } # Output Result $apiPath } function Get-AccessToken { <# .SYNOPSIS Get access token .DESCRIPTION Get currently registered access token .EXAMPLE PS C:\> Get-AccessToken Get currently registered access token #> [cmdletbinding(ConfirmImpact="Low")] param() Write-PSFMessage -Level System -Message "Retrieve token object Id '$($script:PersonioToken.TokenID)'" -Tag "AccessToken", "Get" $script:PersonioToken } function New-AccessToken { <# .SYNOPSIS Create access token .DESCRIPTION Create access token .PARAMETER RawToken The RawToken data from personio service .EXAMPLE PS C:\> New-AccessToken -RawToken $rawToken Creates a Personio.Core.AccessToken from variable $rawToken #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [cmdletbinding(PositionalBinding = $true)] [OutputType([Personio.Core.AccessToken])] param( [Parameter(Mandatory = $true)] [string] $RawToken ) # Convert token to data object Write-PSFMessage -Level System -Message "Decode token data" -Tag "AccessToken", "Create" $tokenInfo = ConvertFrom-JWTtoken -Token $RawToken # Create output token Write-PSFMessage -Level System -Message "Creating Personio.Core.AccessToken object" -Tag "AccessToken", "Create" $token = New-Object -TypeName Personio.Core.AccessToken -ArgumentList @{ TokenID = $tokenInfo.JwtId ClientId = $tokenInfo.ClientId ApplicationId = $applicationIdentifier ApplicationPartnerId = $partnerIdentifier Issuer = $tokenInfo.Issuer Scope = $tokenInfo.Scope Token = ($RawToken | ConvertTo-SecureString -AsPlainText -Force) ApiUri = "$(Get-PSFConfigValue -FullName 'PSPersonio.API.URI' -Fallback $tokenInfo.Issuer)" TimeStampCreated = $tokenInfo.IssuedUTC.ToLocalTime() TimeStampNotBefore = $tokenInfo.NotBeforeUTC.ToLocalTime() TimeStampExpires = $tokenInfo.ExpiresUTC.ToLocalTime() TimeStampModified = Get-Date } # Output object $token } function New-PS1XML { <# .SYNOPSIS Register access token .DESCRIPTION Register access token within the module .PARAMETER Path The filename of the ps1xml file to create .PARAMETER TypeName Name of the type to create format file .PARAMETER PropertyList Name list of properties to put in format file .PARAMETER View The view to create in the format file .PARAMETER Encoding File encoding .PARAMETER PassThru Outputs the token to the console, even when the register switch is set .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> New-PS1XML -Path C:\MyObject.Format.ps1xml -TypeName MY.Object -PropertyList $PropertyList Create MyObject.Format.ps1xml in C:\ with TypeFormat on object My.Object with property names set in $PropertyList #> [cmdletbinding( PositionalBinding = $true, SupportsShouldProcess = $true, ConfirmImpact = 'Medium' )] param( [ValidateNotNullOrEmpty()] [Parameter(Mandatory = $true)] [string] $Path, [ValidateNotNullOrEmpty()] [Parameter(Mandatory = $true)] [string] $TypeName, [ValidateNotNullOrEmpty()] [Parameter(Mandatory = $true)] [string[]] $PropertyList, [ValidateNotNullOrEmpty()] [ValidateSet("Table", "List", "Wide", "All")] [string[]] $View = "All", [ValidateNotNullOrEmpty()] [ValidateSet("UTF8", "UTF32", "UTF7", "Default", "Unicode", "ASCII", "BigEndianUnicode")] [string] $Encoding = "UTF8", [switch] $PassThru ) # check Path Write-PSFMessage -Level Verbose -Message "Validate path: $($Path)" -Tag "FormatType", "Format.ps1xml" if (-not (Test-Path -Path $Path -IsValid -PathType Leaf)) { Stop-PSFFunction -Message "Path $($Path)) is not valid" -Tag "FormatType", "Format.ps1xml" -Cmdlet $pscmdlet } $tempPath = Join-Path -Path $env:TEMP -ChildPath "$((New-Guid).guid).format.ps1xml" Write-PSFMessage -Level System -Message "Start writing xml data in temporary file '$($tempPath)' ($($Encoding) encoding)" -Tag "FormatType", "Format.ps1xml", "tempfile" $XmlWriter = [System.XMl.XmlTextWriter]::new($tempPath, [System.Text.Encoding]::$Encoding) $xmlWriter.Formatting = "Indented" $xmlWriter.Indentation = "4" $xmlWriter.WriteStartDocument() #region <Configuration><ViewDefinitions> $xmlWriter.WriteStartElement("Configuration") $xmlWriter.WriteStartElement("ViewDefinitions") if ($View -like "Table" -or $View -like "All") { Write-PSFMessage -Level Verbose -Message "Generate table view for type $($TypeName)" -Tag "FormatType", "Format.ps1xml", "TableView" #region Start <View> $xmlWriter.WriteStartElement("View") # Element <Name> $xmlWriter.WriteElementString("Name", "Table_$($TypeName)") #region Start <ViewSelectedBy> $xmlWriter.WriteStartElement("ViewSelectedBy") $xmlWriter.WriteElementString("TypeName", "$($TypeName)") $xmlWriter.WriteEndElement() #endregion End <ViewSelectedBy> #region Start <TableControl> $xmlWriter.WriteStartElement("TableControl") # Element <AutoSize> $xmlWriter.WriteStartElement("AutoSize") $xmlWriter.WriteEndElement() #region Start <TableHeaders> $xmlWriter.WriteStartElement("TableHeaders") #region Start <TableColumnHeader> foreach ($property in $PropertyList) { $xmlWriter.WriteStartElement("TableColumnHeader") $xmlWriter.WriteElementString("Label", "$($property)") $xmlWriter.WriteEndElement() } #endregion End <TableColumnHeader> $xmlWriter.WriteEndElement() #endregion End <TableHeaders> #region Start <TableRowEntries><TableRowEntry><TableColumnItems> $xmlWriter.WriteStartElement("TableRowEntries") $xmlWriter.WriteStartElement("TableRowEntry") $xmlWriter.WriteStartElement("TableColumnItems") #region Start <TableColumnItem> <PropertyName> foreach ($property in $PropertyList) { $xmlWriter.WriteStartElement("TableColumnItem") $xmlWriter.WriteElementString("PropertyName", $property) $xmlWriter.WriteEndElement() } #endregion end <TableColumnItem> <PropertyName> $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() #endregion End <TableRowEntries><TableRowEntry><TableColumnItems> $xmlWriter.WriteEndElement() #endregion End <TableControl> $xmlWriter.WriteEndElement() #endregion End <View> } if ($View -like "List" -or $View -like "All") { Write-PSFMessage -Level Verbose -Message "Generate list view for type $($TypeName)" -Tag "FormatType", "Format.ps1xml", "ListView" #region Start <View> $xmlWriter.WriteStartElement("View") # Element <Name> $xmlWriter.WriteElementString("Name", "List_$($TypeName)") #region Start <ViewSelectedBy> $xmlWriter.WriteStartElement("ViewSelectedBy") $xmlWriter.WriteElementString("TypeName", "$($TypeName)") $xmlWriter.WriteEndElement() #endregion End <ViewSelectedBy> #region Start <ListControl><ListEntries><ListEntry><ListItems> $xmlWriter.WriteStartElement("ListControl") $xmlWriter.WriteStartElement("ListEntries") $xmlWriter.WriteStartElement("ListEntry") $xmlWriter.WriteStartElement("ListItems") #region Start <ListItem> <PropertyName> foreach ($property in $PropertyList) { $xmlWriter.WriteStartElement("ListItem") $xmlWriter.WriteElementString("PropertyName", $property) $xmlWriter.WriteEndElement() } #endregion End <ListItem> <PropertyName> $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() #endregion End <ListControl><ListEntries><ListEntry><ListItems> $xmlWriter.WriteEndElement() #endregion End <View> } if ($View -like "Wide" -or $View -like "All") { Write-PSFMessage -Level Verbose -Message "Generate wide view for type $($TypeName)" -Tag "FormatType", "Format.ps1xml", "WideView" #region Start <View> $xmlWriter.WriteStartElement("View") # Element <Name> $xmlWriter.WriteElementString("Name", "Wide_$($TypeName)") #region Start <ViewSelectedBy> $xmlWriter.WriteStartElement("ViewSelectedBy") $xmlWriter.WriteElementString("TypeName", "$($TypeName)") $xmlWriter.WriteEndElement() #endregion End <ViewSelectedBy> #region Start <WideControl><WideEntries><WideEntry> $xmlWriter.WriteStartElement("WideControl") $xmlWriter.WriteElementString("AutoSize", "") $xmlWriter.WriteStartElement("WideEntries") $xmlWriter.WriteStartElement("WideEntry") #region Start <WideItem> <PropertyName> $wideProperty = "" $wideProperty = $PropertyList | Where-Object { $_ -like "*name*"} | Sort-Object | Select-Object -First 1 if(-not $wideProperty) {$wideProperty = $PropertyList | Where-Object { $_ -like "Id"} | Sort-Object | Select-Object -First 1} if(-not $wideProperty) {$wideProperty = $PropertyList | Select-Object -First 1} $xmlWriter.WriteStartElement("WideItem") $xmlWriter.WriteElementString("PropertyName", $wideProperty) $xmlWriter.WriteEndElement() #endregion End <WideItem> <PropertyName> $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() #endregion End <WideControl><WideEntries><WideEntry> $xmlWriter.WriteEndElement() #endregion End <View> } $xmlWriter.WriteEndElement() $xmlWriter.WriteEndElement() #endregion <Configuration><ViewDefinitions> # End the XML Document $xmlWriter.WriteEndDocument() # Finish The Document $xmlWriter.Finalize $xmlWriter.Flush() $xmlWriter.Close() # Write file if ($pscmdlet.ShouldProcess("TypeFormat file '$($Path)' for type [$($TypeName)] with properties '$([string]::Join(", ", $PropertyList))'", "New")) { Write-PSFMessage -Level Verbose -Message "New TypeFormat file '$($Path)' for type [$($TypeName)] with properties '$([string]::Join(", ", $PropertyList))'" -Tag "FormatType", "Format.ps1xml", "New" $output = Move-Item -Path $tempPath -Destination $Path -Force -Confirm:$false -PassThru if($PassThru) { $output | Get-Item } } } function Register-AccessToken { <# .SYNOPSIS Register access token .DESCRIPTION Register access token within the module .PARAMETER Token The Token object to register .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Register-AccessToken -Token $token Register the Personio.Core.AccessToken from variable $rawToken to module wide vaiable $PersonioToken #> [cmdletbinding( PositionalBinding=$true, SupportsShouldProcess = $true )] param( [Parameter(Mandatory = $true)] [Personio.Core.AccessToken] $Token ) # check if $PersonioToken already has data if($PersonioToken.TokenID) { Write-PSFMessage -Level System -Message "Replacing existing token object with Id '$($PersonioToken.TokenID)' with new token '$($Token.TokenID)' (valid until $($Token.TimeStampExpires))" -Tag "AccessToken", "Register" } else { Write-PSFMessage -Level System -Message "Register token '$($Token.TokenID)' (valid until $($Token.TimeStampExpires))" -Tag "AccessToken", "Register" } # register token if ($pscmdlet.ShouldProcess("AccessToken for ClientId '$($Token.ClientId)' from '$($Token.Issuer)' valid until $($Token.TimeStampExpires)", "Register")) { $script:PersonioToken = $Token } } function Get-PERSAbsence { <# .Synopsis Get-PERSAbsence .DESCRIPTION Retrieve absence periods from Personio tracked in days Parameters for filtered by period and/or specific employee(s) are available. The result can be paginated and. .PARAMETER InputObject AbsencePeriod to call again .PARAMETER StartDate First day of the period to be queried. .PARAMETER EndDate Last day of the period to be queried. .PARAMETER UpdatedFrom Query the periods that created or modified from the date UpdatedFrom. .PARAMETER UpdatedTo Query the periods that created or modified until the date UpdatedTo .PARAMETER EmployeeId A list of Personio employee ID's to filter the result. The result filters including only absences of provided employees .PARAMETER InclusiveFiltering If specified, datefiltering will change it's behaviour Absence records that begin or end before specified StartDate or after specified EndDate will be outputted .PARAMETER ResultSize How much records will be returned from the api. Default is 200. Use this parameter, when function throw information about pagination .PARAMETER Token AccessToken object for Personio service .EXAMPLE PS C:\> Get-PERSAbsence Get all available absence periods (api-side-pagination will kick in at 200) .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( DefaultParameterSetName = "Default", SupportsShouldProcess = $false, PositionalBinding = $true, ConfirmImpact = 'Low' )] Param( [Parameter(ParameterSetName = "Default")] [datetime] $StartDate, [Parameter(ParameterSetName = "Default")] [datetime] $EndDate, [Parameter(ParameterSetName = "Default")] [datetime] $UpdatedFrom, [Parameter(ParameterSetName = "Default")] [datetime] $UpdatedTo, [Parameter( ParameterSetName = "Default", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [ValidateNotNullOrEmpty()] [int[]] $EmployeeId, [Parameter(ParameterSetName = "Default")] [ValidateNotNullOrEmpty()] [int] $ResultSize, [Parameter( ParameterSetName = "ByType", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [Personio.Absence.AbsencePeriod[]] $InputObject, [switch] $InclusiveFiltering, [ValidateNotNullOrEmpty()] [Personio.Core.AccessToken] $Token ) begin { # define query parameters $queryParameter = [ordered]@{} # fill query parameters if ($ResultSize) { $queryParameter.Add("limit", $ResultSize) $queryParameter.Add("offset", 0) } if ($StartDate) { $queryParameter.Add("start_date", (Get-Date -Date $StartDate -Format "yyyy-MM-dd")) } if ($EndDate) { $queryParameter.Add("end_date", (Get-Date -Date $EndDate -Format "yyyy-MM-dd")) } if ($UpdatedFrom) { $queryParameter.Add("updated_from", (Get-Date -Date $UpdatedFrom -Format "yyyy-MM-dd")) } if ($UpdatedTo) { $queryParameter.Add("updated_to", (Get-Date -Date $UpdatedTo -Format "yyyy-MM-dd")) } } process { if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } $parameterSetName = $pscmdlet.ParameterSetName Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "AbsensePeriod" # fill pipedin query parameters if ($EmployeeId) { $queryParameter.Add("employees[]", [array]$EmployeeId) } # Prepare query $invokeParam = @{ "Type" = "GET" "ApiPath" = "company/time-offs" "Token" = $Token } if ($queryParameter) { $invokeParam.Add("QueryParameter", $queryParameter) } # Execute query $responseList = [System.Collections.ArrayList]@() if ($parameterSetName -like "Default") { Write-PSFMessage -Level Verbose -Message "Getting available absence periods" -Tag "AbsensePeriod", "Query" $response = Invoke-PERSRequest @invokeParam # Check response and add to responseList if ($response.success) { $null = $responseList.Add($response) } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsensePeriod", "Query" } } elseif ($parameterSetName -like "ByType") { foreach ($absencePeriod in $InputObject) { Write-PSFMessage -Level Verbose -Message "Getting absence period Id $($absencePeriod.Id)" -Tag "AbsensePeriod", "Query" $invokeParam.ApiPath = "company/time-offs/$($absencePeriod.Id)" $response = Invoke-PERSRequest @invokeParam # Check respeonse and add to responeList if ($response.success) { $null = $responseList.Add($response) } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data on absence Id $($absencePeriod.Id)" -Tag "AbsensePeriod", "Query" } # remove token param for further api calls, due to the fact, that the passed in token, is no more valid after previous api all (api will use internal registered token) if ($InputObject.Count -gt 1) { $invokeParam.Remove("Token") } } } Remove-Variable -Name response -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore foreach ($response in $responseList) { # Check pagination / result limitation if ($response.metadata) { Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "AbsensePeriod", "Query", "WebRequest", "Pagination" } # Process result $output = [System.Collections.ArrayList]@() foreach ($record in $response.data) { Write-PSFMessage -Level Debug -Message "Working on record Id $($record.attributes.id) startDate: $($record.attributes.start_date) - endDate: $($record.attributes.end_date)" -Tag "AbsensePeriod", "ObjectCreation" # Create object $result = [Personio.Absence.AbsencePeriod]@{ BaseObject = $record.attributes Id = $record.attributes.id } $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)") $result.Employee = [Personio.Employee.BasicEmployee]@{ BaseObject = $record.attributes.employee.attributes Id = $record.attributes.employee.attributes.id.value Name = "$($record.attributes.employee.attributes.last_name.value), $($record.attributes.employee.attributes.first_name.value)" } $result.Employee.psobject.TypeNames.Insert(1, "Personio.Employee.$($record.attributes.employee.type)") $result.Type = [Personio.Absence.AbsenceType]@{ BaseObject = $record.attributes.time_off_type.attributes Id = $record.attributes.time_off_type.attributes.id Name = $record.attributes.time_off_type.attributes.name } $result.Type.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.attributes.time_off_type.type)") # add objects to output array $null = $output.Add($result) } Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) objects of type [Personio.Absence.AbsencePeriod]" -Tag "AbsensePeriod", "Result" # Filtering if (-not $MyInvocation.BoundParameters['InclusiveFiltering']) { if ($StartDate) { $output = $output | Where-Object StartDate -ge $StartDate } if ($EndDate) { $output = $output | Where-Object EndDate -le $EndDate } if ($UpdatedFrom) { $output = $output | Where-Object UpdatedAt -ge $UpdatedFrom } if ($UpdatedTo) { $output = $output | Where-Object UpdatedAt -le $UpdatedTo } } # output final results Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenseType", "Result", "Output" $output } # Cleanup variable Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore $queryParameter.remove('employees[]') } end { } } function Get-PERSAbsenceSummary { <# .Synopsis Get-PERSAbsenceSummary .DESCRIPTION Retrieve absence summery for a specific employee from Personio .PARAMETER Employee The employee to get the summary for .PARAMETER EmployeeId Employee ID to get the summary for .PARAMETER Filter The name of the absence type to filter on .PARAMETER IncludeZeroValues If this is specified, all the absence types will be outputted. Be default, only absence summary records with a balance value greater than 0 are returned .PARAMETER Token AccessToken object for Personio service .EXAMPLE PS C:\> Get-PERSAbsenceSummary -EmployeeId 111 Get absence summary of all types on employee with ID 111 .EXAMPLE PS C:\> Get-PERSAbsenceSummary -Employee (Get-PERSEmployee -Email john.doe@company.com) Get absence summary of all types on employee John Doe .EXAMPLE PS C:\> Get-PERSEmployee -Email john.doe@company.com | Get-PERSAbsenceSummary -Type "Vacation" Get absence summary of type 'vacation' on employee John Doe .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( DefaultParameterSetName = "ApiNative", SupportsShouldProcess = $false, PositionalBinding = $true, ConfirmImpact = 'Medium' )] Param( [Parameter( ParameterSetName = "UserFriendly", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [Personio.Employee.BasicEmployee[]] $Employee, [Parameter( ParameterSetName = "ApiNative", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [int[]] $EmployeeId, [Alias("Type", "AbsenceType")] [string[]] $Filter, [switch] $IncludeZeroValues, [Personio.Core.AccessToken] $Token ) begin { if ($MyInvocation.BoundParameters['Token']) { $absenceTypes = Get-PERSAbsenceType -Token $Token } else { $absenceTypes = Get-PERSAbsenceType } $newTokenRequired = $true } process { # collect Employees from piped in IDs if ($MyInvocation.BoundParameters['EmployeeId']) { $Employee = Get-PERSEmployee -InputObject $EmployeeId } # Process employees and gather data $output = [System.Collections.ArrayList]@() foreach ($employeeItem in $Employee) { # Prepare token if (-not $MyInvocation.BoundParameters['Token'] -or $newTokenRequired) { $Token = Get-AccessToken } # Prepare query $invokeParam = @{ "Type" = "GET" "ApiPath" = "company/employees/$($employeeItem.id)/absences/balance" "Token" = $Token } # Execute query Write-PSFMessage -Level Verbose -Message "Getting absence summary for '$($employeeItem)'" -Tag "AbsenceSummary", "Query" $response = Invoke-PERSRequest @invokeParam # Check respeonse if ($response.success) { # Process result foreach ($record in $response.data) { Write-PSFMessage -Level Debug -Message "Working on record $($record.name) (ID: $($record.id)) for '$($employeeItem)'" -Tag "AbsenceSummary", "ObjectCreation" # process if filter is not specified or filter applies on record if ((-not $Filter) -or ($Filter | ForEach-Object { $record.name -like $_ })) { # Create object $result = [Personio.Absence.AbsenceSummaryRecord]@{ BaseObject = $record AbsenceType = ($absenceTypes | Where-Object Id -eq $record.id) Employee = $employeeItem "Category" = $record.category "Balance" = $record.balance } $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)") # add objects to output array $null = $output.Add($result) } } } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsenceSummary", "Query" } } Write-PSFMessage -Level System -Message "Retrieve $($output.Count) objects of type [Personio.Absence.AbsenceSummaryRecord]" -Tag "AbsenceSummary", "Result" if (-not $MyInvocation.BoundParameters['IncludeZeroValues']) { $output = $output | Where-Object Balance -gt 0 } # output final results Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenceSummary", "Result", "Output" $output # Cleanup variable Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore } end { } } function Get-PERSAbsenceType { <# .Synopsis Get-PERSAbsenceType .DESCRIPTION Retrieve absence types from Personio .PARAMETER Name Name filter for absence types .PARAMETER Id Id filter for absence types .PARAMETER ResultSize How much records will be returned from the api. Default is 200. Use this parameter, when function throw information about pagination .PARAMETER Token AccessToken object for Personio service .EXAMPLE PS C:\> Get-PERSAbsenceType Get all available absence types .EXAMPLE PS C:\> Get-PERSAbsenceType -Name "Krankheit*" Get all available absence types with name "Krankheit*" .EXAMPLE PS C:\> Get-PERSAbsenceType -Id 10 Get absence types with id 10 .EXAMPLE PS C:\> Get-PERSAbsenceType -Id 10, 11, 12 -Name "*Krankheit*" Get absence types with id 10, 11, 12 as long, as name matches *Krankheit* .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( SupportsShouldProcess = $false, PositionalBinding = $true, ConfirmImpact = 'Low' )] Param( [string[]] $Name, [int[]] $Id, [int] $ResultSize, [Personio.Core.AccessToken] $Token ) begin { } process { } end { if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } # Prepare query $invokeParam = @{ "Type" = "GET" "ApiPath" = "company/time-off-types" "Token" = $Token } if ($ResultSize) { $invokeParam.Add( "QueryParameter", @{ "limit" = $ResultSize "offset" = 0 } ) } # Execute query Write-PSFMessage -Level Verbose -Message "Getting available absence types" -Tag "AbsenseType", "Query" $response = Invoke-PERSRequest @invokeParam # Check respeonse if ($response.success) { # Check pagination / result limitation if ($response.metadata) { Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "AbsenseType", "Query", "WebRequest", "Pagination" } # Process result $output = [System.Collections.ArrayList]@() foreach ($record in $response.data) { Write-PSFMessage -Level Debug -Message "Working on record $($record.attributes.name) (ID: $($record.attributes.id))" -Tag "AbsenseType", "ObjectCreation" # Create object $result = [Personio.Absence.AbsenceType]@{ BaseObject = $record.attributes Id = $record.attributes.id Name = $record.attributes.name } $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)") # add objects to output array $null = $output.Add($result) } Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) objects of type [Personio.Absence.AbsenceType]" -Tag "AbsenseType", "Result" # Filtering if ($Name -and $output) { Write-PSFMessage -Level Verbose -Message "Filter by Name: $([string]::Join(", ", $Name))" -Tag "AbsenseType", "Filtering", "NameFilter" $newOutput = [System.Collections.ArrayList]@() foreach ($item in $output) { foreach ($filter in $Name) { $filterResult = $item | Where-Object Name -like $filter if ($filterResult) { $null = $newOutput.Add($filterResult) } } } $Output = $newOutput Remove-Variable -Name newOutput, filter, filterResult, item -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore } if ($Id -and $output) { Write-PSFMessage -Level Verbose -Message "Filter by Id: $([string]::Join(", ", $Id))" -Tag "AbsenseType", "Filtering", "IdFilter" $output = $output | Where-Object Id -in $Id } # output final results Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenseType", "Result", "Output" $output } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsenseType", "Query" } } } function New-PERSAbsence { <# .Synopsis New-PERSAbsence .DESCRIPTION Adds absence period (tracked in days) into Personio service .PARAMETER Employee The employee to create a absence for .PARAMETER EmployeeId Employee ID to create an absence .PARAMETER AbsenceType The Absence type to create .PARAMETER AbsenceTypeId The Absence type to create .PARAMETER StartDate First day of absence period .PARAMETER EndDate Last day of absence period .PARAMETER HalfDayStart Weather the start date is a half-day off .PARAMETER HalfDayEnd Weather the end date is a half-day off .PARAMETER Comment Optional comment for the absence .PARAMETER SkipApproval Optional, default value is true. If set to false, the approval status of the absence request will be "pending" if an approval rule is set for the absence type in Personio. The respective approval flow will be triggered. .PARAMETER Token AccessToken object for Personio service .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> New-PERSAbsence -Employee (Get-PERSEmployee -Email john.doe@company.com) -Type (Get-PERSAbsenceType -Name "Vacation") -StartDate 01.01.2023 -EndDate 05.01.2023 Create a new absence for "John Doe" of type "Urlaub" from 01.01.2023 until 05.01.2023 .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( DefaultParameterSetName = "ApiNative", SupportsShouldProcess = $true, PositionalBinding = $true, ConfirmImpact = 'Medium' )] [OutputType([Personio.Absence.AbsencePeriod])] Param( [Parameter( ParameterSetName = "UserFriendly", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [Personio.Employee.BasicEmployee] $Employee, [Parameter( ParameterSetName = "ApiNative", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [int] $EmployeeId, [Parameter( ParameterSetName = "UserFriendly", Mandatory = $true )] [Alias("Type", "Absence")] [Personio.Absence.AbsenceType] $AbsenceType, [Parameter( ParameterSetName = "ApiNative", Mandatory = $true )] [Alias("TypeId")] [int] $AbsenceTypeId, [Parameter(Mandatory = $true)] [datetime] $StartDate, [Parameter(Mandatory = $true)] [datetime] $EndDate, [bool] $HalfDayStart = $false, [bool] $HalfDayEnd = $false, [string] $Comment, [ValidateNotNullOrEmpty()] [bool] $SkipApproval = $true, [ValidateNotNullOrEmpty()] [Personio.Core.AccessToken] $Token ) begin { } process { if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } $body = [ordered]@{} $parameterSetName = $pscmdlet.ParameterSetName Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "AbsensePeriod", "New" # fill pipedin query parameters if ($parameterSetName -like "ApiNative") { $body.Add("employee_id", $EmployeeId) $body.Add("time_off_type_id", $AbsenceTypeId) } elseif ($parameterSetName -like "UserFriendly") { $body.Add("employee_id", $Employee.Id) $body.Add("time_off_type_id", $AbsenceType.Id) } # fill query parameters $body.Add("start_date", (Get-Date -Date $StartDate -Format "yyyy-MM-dd")) $body.Add("end_date", (Get-Date -Date $EndDate -Format "yyyy-MM-dd")) $body.Add("half_day_start", $HalfDayStart.ToString().ToLower()) $body.Add("half_day_end", $HalfDayEnd.ToString().ToLower()) $body.Add("skip_approval", $SkipApproval.ToString().ToLower()) #if ($MyInvocation.BoundParameters['Comment']) { $body.Add("comment", [uri]::EscapeDataString($Comment)) } if ($MyInvocation.BoundParameters['Comment']) { $body.Add("comment", $Comment) } # Debug logging foreach ($key in $body.Keys) { Write-PSFMessage -Level Debug -Message "Added body attribute '$($key)' with value '$($body[$key])'" -Tag "AbsensePeriod", "New", "Request" } # Prepare query $invokeParam = @{ "Type" = "POST" "ApiPath" = "company/time-offs" "Token" = $Token "Body" = $body "AdditionalHeader" = @{ "accept" = "application/json" "content-type" = "application/x-www-form-urlencoded" } } $processMsg = "absence period of '$($AbsenceType.Name)' ($($body['start_date'])-$($body['end_date']))" if ($pscmdlet.ShouldProcess($processMsg, "New")) { Write-PSFMessage -Level Verbose -Message "New $($processMsg)" -Tag "AbsensePeriod", "New" # Execute query $response = Invoke-PERSRequest @invokeParam # Check response and add to responseList if ($response.success) { Write-PSFMessage -Level System -Message "Retrieve $(([array]$response.data).Count) objects" -Tag "AbsensePeriod", "New", "Result" foreach ($record in $response.data) { # create absence object $result = [Personio.Absence.AbsencePeriod]@{ BaseObject = $record.attributes Id = $record.attributes.id } $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)") # make employee record to valid object $result.Employee = [Personio.Employee.BasicEmployee]@{ BaseObject = $record.attributes.employee.attributes Id = $record.attributes.employee.attributes.id.value Name = "$($record.attributes.employee.attributes.last_name.value), $($record.attributes.employee.attributes.first_name.value)" } $result.Employee.psobject.TypeNames.Insert(1, "Personio.Employee.$($record.attributes.employee.type)") # make absenceType record to valid object $result.Type = [Personio.Absence.AbsenceType]@{ BaseObject = $record.attributes.time_off_type.attributes Id = $record.attributes.time_off_type.attributes.id Name = $record.attributes.time_off_type.attributes.name } $result.Type.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.attributes.time_off_type.type)") # output final results Write-PSFMessage -Level Verbose -Message "Output [$($result.psobject.TypeNames[0])] object '$($result.Type)' (start: $(Get-Date $result.StartDate -Format "yyyy-MM-dd") - end: $(Get-Date $result.EndDate -Format "yyyy-MM-dd"))" -Tag "AbsensePeriod", "Result", "Output" $result } } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsensePeriod", "New" } } # Cleanup variable Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore $body.remove('employee_id') } end { } } function Remove-PERSAbsence { <# .Synopsis Remove-PERSAbsence .DESCRIPTION Remove absence period (tracked in days) from Personio service .PARAMETER Absence The Absence to remove .PARAMETER AbsenceId The ID of the absence to remove .PARAMETER Force Suppress the user confirmation. .PARAMETER Token AccessToken object for Personio service .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> $absence | Remove-PERSAbsence Remove absence from variable $absence. Assuming that $absence was previsouly filled with Get-PERSAbsence .EXAMPLE PS C:\> $absence | Remove-PERSAbsence -Force Remove absence from variable $absence silently. (Confirmation will be suppressed) .EXAMPLE PS C:\> Remove-PERSAbsence -AbsenceId 111 Remove absence with ID 111 .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( DefaultParameterSetName = "ApiNative", SupportsShouldProcess = $true, PositionalBinding = $true, ConfirmImpact = 'High' )] Param( [Parameter( ParameterSetName = "UserFriendly", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [Personio.Absence.AbsencePeriod] $Absence, [Parameter( ParameterSetName = "ApiNative", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [int] $AbsenceId, [switch] $Force, [ValidateNotNullOrEmpty()] [Personio.Core.AccessToken] $Token ) begin { } process { if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } $parameterSetName = $pscmdlet.ParameterSetName Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "AbsensePeriod", "Remove" # fill pipedin query parameters if ($parameterSetName -like "ApiNative") { $id = $AbsenceId } elseif ($parameterSetName -like "UserFriendly") { $id = $Absence.Id } # Prepare query $invokeParam = @{ "Type" = "DELETE" "ApiPath" = "company/time-offs/$($id)" "Token" = $Token } $processMessage = "absence id '$($id)'" if($parameterSetName -like "UserFriendly") { $processMessage = $processMessage + " (" + $Absence.Type + " on '" + $Absence.Employee + "' for " + (Get-Date -Date $Absence.StartDate -Format "yyyy-MM-dd") + " - " + (Get-Date -Date $Absence.EndDate -Format "yyyy-MM-dd") + ")" } if(-not $Force) { if ($pscmdlet.ShouldProcess($processMessage, "Remove")) { $Force = $true } } if($Force) { Write-PSFMessage -Level Verbose -Message "Remove $($processMessage)" -Tag "AbsensePeriod", "Remove" # Execute query $response = Invoke-PERSRequest @invokeParam # Check response and add to responseList if ($response.success) { Write-PSFMessage -Level Verbose -Message "Absence id '$($id)' was removed. Message: $($response.data.message)" -Tag "AbsensePeriod", "Remove", "Result" } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsensePeriod", "Remove", "Result" } } # Cleanup variable Remove-Variable -Name Token,id, doRemove, processMessage -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore } end { } } function Get-PERSAttendance { <# .Synopsis Get-PERSAttendance .DESCRIPTION Retrieve attendance data for the company employees Parameters for filtered by period and/or specific employee(s) are available The result can be paginated .PARAMETER StartDate First day to be queried .PARAMETER EndDate Last day to be queried .PARAMETER UpdatedFrom Query the periods that created or modified from the updated date .PARAMETER UpdatedTo Query the periods that created or modified until the updated date .PARAMETER EmployeeId A list of Personio employee ID's to filter the result. The result filters including only attendance of provided employees .PARAMETER IncludePending Returns attendance data with a status of pending, rejected and confirmed. For pending periods, the EndDate attribute is nullable. The status of each period is included in the response. .PARAMETER InclusiveFiltering If specified, datefiltering will change it's behaviour Attendance data records that begin or end before specified StartDate or after specified EndDate will be outputted .PARAMETER ResultSize How much records will be returned from the api Default is 200 Use this parameter, when function throw information about pagination .PARAMETER Token AccessToken object for Personio service .EXAMPLE PS C:\> Get-PERSAttendance -StartDate 2023-01-01 -EndDate 2023-01-31 Get attendance data from 2023-01-01 until 2023-01-31 (api-side-pagination will kick in at 200) .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( SupportsShouldProcess = $false, PositionalBinding = $true, ConfirmImpact = 'Low' )] Param( [Parameter(Mandatory = $true)] [datetime] $StartDate, [Parameter(Mandatory = $true)] [datetime] $EndDate, [ValidateNotNullOrEmpty()] [datetime] $UpdatedFrom, [ValidateNotNullOrEmpty()] [datetime] $UpdatedTo, [Parameter( ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [ValidateNotNullOrEmpty()] [int[]] $EmployeeId, [ValidateNotNullOrEmpty()] [int] $ResultSize, [switch] $InclusiveFiltering, [ValidateNotNullOrEmpty()] [bool] $IncludePending = $true, [ValidateNotNullOrEmpty()] [Personio.Core.AccessToken] $Token ) begin { # Cache for queried employees $listEmployees = [System.Collections.ArrayList]@() # define query parameters $_startDate = Get-Date -Date $StartDate -Format "yyyy-MM-dd" $_endDate = Get-Date -Date $EndDate -Format "yyyy-MM-dd" $queryParameter = [ordered]@{ "start_date" = $_startDate "end_date" = $_endDate } # fill query parameters if ($MyInvocation.BoundParameters['UpdatedFrom']) { $queryParameter.Add("updated_from", (Get-Date -Date $UpdatedFrom -Format "yyyy-MM-dd")) } if ($MyInvocation.BoundParameters['UpdatedTo']) { $queryParameter.Add("updated_to", (Get-Date -Date $UpdatedTo -Format "yyyy-MM-dd")) } if ($MyInvocation.BoundParameters['ResultSize']) { $queryParameter.Add("limit", $ResultSize) $queryParameter.Add("offset", 0) } } process { # basic preparation if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } $parameterSetName = $pscmdlet.ParameterSetName Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Attendance" # fill pipedin query parameters if ($MyInvocation.BoundParameters['EmployeeId'] -and $EmployeeId) { $queryParameter.Add("employees[]", $EmployeeId) } # Prepare query $invokeParam = @{ "Type" = "GET" "ApiPath" = "company/attendances" "Token" = $Token } if ($queryParameter) { $invokeParam.Add("QueryParameter", $queryParameter) } # Execute query Write-PSFMessage -Level Verbose -Message "Getting available attendance periods from $_startDate to $_endDate" -Tag "Attendance", "Query" $response = Invoke-PERSRequest @invokeParam # Check response and add to responseList if (-not $response.success) { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "Attendance", "Query" } # Check pagination / result limitation if ($response.metadata.total_elements -gt $response.limit) { Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "Attendance", "Query", "WebRequest", "Pagination" } # Process result $output = [System.Collections.ArrayList]@() foreach ($record in $response.data) { Write-PSFMessage -Level Debug -Message "Working on record Id $($record.attributes.id) startDate: $($record.attributes.start_date) - endDate: $($record.attributes.end_date)" -Tag "Attendance", "ObjectCreation" # Create object $result = [Personio.Attendance.AttendanceRecord]@{ BaseObject = $record.attributes Id = $record.id } $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)") # insert employee if ($listEmployees -and ($record.attributes.employee -in $listEmployees.Id)) { $_employee = $listEmployees | Where-Object Id -eq $record.attributes.employee } else { $_employee = Get-PERSEmployee -InputObject $record.attributes.employee | Select-Object -First 1 $null = $listEmployees.Add($_employee) } $result.Employee = $_employee Remove-Variable -Name _employee -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore #$result.Project = Get-PERSProject -InputObject $record.attributes.project # add objects to output array $null = $output.Add($result) } Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) objects of type [Personio.Attendance.AttendanceRecord]" -Tag "Attendance", "Result" # Filtering if (-not $MyInvocation.BoundParameters['InclusiveFiltering']) { if ($StartDate) { $output = $output | Where-Object Date -ge $StartDate } if ($EndDate) { $output = $output | Where-Object Date -le $EndDate } if ($UpdatedFrom) { $output = $output | Where-Object UpdatedAt -ge $UpdatedFrom } if ($UpdatedTo) { $output = $output | Where-Object UpdatedAt -le $UpdatedTo } } # output final results Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenseType", "Result", "Output" $output # Cleanup variable Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore $queryParameter.remove('employees[]') } end { } } function New-PERSAttendance { <# .Synopsis New-PERSAttendance .DESCRIPTION Add attendance records for the company employees into Personio service .PARAMETER Employee The employee to create a absence for .PARAMETER EmployeeId Employee ID to create an absence .PARAMETER Project The project to book on the attendance .PARAMETER ProjectId The id of the project to book on the attendance .PARAMETER Start Start of the attendance record as a datetime or parseable string value If only a time value is specified, the record will be today with the specified time. Attention, the date value of start and end has to be the same day! .PARAMETER End Start of the attendance record as a datetime or parseable string value If only a time value is specified, the record will be today with the specified time. Attention, the date value of start and end has to be the same day! .PARAMETER Break Minutes of break within the attendance record .PARAMETER Comment Optional comment for the attendance .PARAMETER SkipApproval Optional, default value is true. If set to false, the approval status of the attendance will be "pending" The respective approval flow will be triggered. .PARAMETER Token AccessToken object for Personio service .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> New-PERSAttendance -Employee (Get-PERSEmployee -Email john.doe@company.com) -Start 08:00 -End 12:00 Create a new attendance record for "John Doe" for "today" from 8 - 12am .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( DefaultParameterSetName = "ApiNative", SupportsShouldProcess = $true, PositionalBinding = $true, ConfirmImpact = 'Medium' )] Param( [Parameter( ParameterSetName = "UserFriendly", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [Personio.Employee.BasicEmployee[]] $Employee, [Parameter( ParameterSetName = "ApiNative", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [int[]] $EmployeeId, [Parameter( ParameterSetName = "UserFriendly", Mandatory = $false )] [Personio.Project.ProjectRecord] $Project, [Parameter( ParameterSetName = "ApiNative", Mandatory = $false )] [int] $ProjectId, [Parameter(Mandatory = $true)] [datetime] $Start, [Parameter(Mandatory = $true)] [datetime] $End, [int] $Break = 0, [string] $Comment, [ValidateNotNullOrEmpty()] [bool] $SkipApproval = $true, [ValidateNotNullOrEmpty()] [Personio.Core.AccessToken] $Token ) begin { $body = [ordered]@{ "attendances" = [System.Collections.ArrayList]@() "skip_approval" = [bool]$SkipApproval } $dateStart = Get-Date -Date $Start -Format "yyyy-MM-dd" $dateEnd = Get-Date -Date $End -Format "yyyy-MM-dd" if ($dateStart -ne $dateEnd) { Stop-PSFFunction -Message "Date problem, Start ($($dateStart)) and Stop ($($dateEnd)) parameters has different date values" -Tag "Attendance", "New", "StartEndDateDifference" -EnableException $true -Cmdlet $pscmdlet } } process { $parameterSetName = $pscmdlet.ParameterSetName Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Attendance", "New" # fill piped in records if ($parameterSetName -like "UserFriendly") { $EmployeeId = $Employee.Id if ($MyInvocation.BoundParameters['Project']) { $ProjectId = $Project.Id } else { $ProjectId = 0 } } # work the pipe/ specified array of employees $attendances = [System.Collections.ArrayList]@() foreach ($employeeIdItem in $EmployeeId) { $attendance = [ordered]@{ "employee" = $employeeIdItem "date" = $dateStart "start_time" = (Get-Date -Date $Start -Format "HH:mm") "end_time" = (Get-Date -Date $End -Format "HH:mm") "break" = [int]$Break } if ($ProjectId) { $attendance.Add("project_id", [int]$ProjectId) } if ($MyInvocation.BoundParameters['Comment']) { $attendance.Add("comment", $Comment) } # Debug logging Write-PSFMessage -Level Debug -Message "Added attendance: $($attendance | ConvertTo-Json -Compress)" -Tag "Attendance", "New", "Request", "Prepare" $null = $attendances.Add($attendance) } $null = $body['attendances'].Add( ($attendances | ForEach-Object { $_ }) ) } end { # Prepare query if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } $invokeParam = @{ "Type" = "POST" "ApiPath" = "company/attendances" "Token" = $Token "Body" = $body "AdditionalHeader" = @{ "accept" = "application/json" "content-type" = "application/json" } } $processMsg = "attendence for $(([array]$body.attendances.employee).count) employee(s)" if ($pscmdlet.ShouldProcess($processMsg, "New")) { Write-PSFMessage -Level Verbose -Message "New $($processMsg)" -Tag "Attendance", "New" # Execute query $response = Invoke-PERSRequest @invokeParam Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore # Check response and add to responseList if ($response.success) { Write-PSFMessage -Level Verbose -Message "Attendance data created. API message: $($response.data.message)" -Tag "Attendance", "New", "Result" # Query attendance data created short before $_attendances = Get-PERSAttendance -StartDate $dateStart -EndDate (Get-Date -Date $Start.AddDays(1) -Format "yyyy-MM-dd") -UpdateFrom (Get-Date).AddMinutes(-5) -UpdateTo (Get-Date) -EmployeeId $body.attendances.employee # Output created attendance records $_attendances | Where-Object id -in $response.data.Id } else { Write-PSFMessage -Level Warning -Message "Personio api reported error: $($response.error)" -Tag "Attendance", "New" } } } } function Remove-PERSAttendance { <# .Synopsis Remove-PERSAttendance .DESCRIPTION Remove attendance data for the company employees from Personio service .PARAMETER Attendance The attendance to remove .PARAMETER AttendanceId The ID of the attendance to remove .PARAMETER SkipApproval Optional, default value is true. If set to false, the approval status within Personio service will be "pending" The respective approval flow will be triggered. .PARAMETER Force Suppress the user confirmation. .PARAMETER Token AccessToken object for Personio service .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> $attendance | Remove-PERSAttendance Remove attendance records from variable $attendance. Assuming that $attendance was previsouly filled with Get-PERSAttendance .EXAMPLE PS C:\> $attendance | Remove-PERSAttendance -Force Remove attendance record from variable $attendance silently. (Confirmation will be suppressed) .EXAMPLE PS C:\> Remove-PERSAttendance -AttendanceId 111 Remove attendance redord with ID 111 .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( DefaultParameterSetName = "ApiNative", SupportsShouldProcess = $true, PositionalBinding = $true, ConfirmImpact = 'High' )] Param( [Parameter( ParameterSetName = "UserFriendly", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [Personio.Attendance.AttendanceRecord] $Attendance, [Parameter( ParameterSetName = "ApiNative", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true )] [int] $AttendanceId, [ValidateNotNullOrEmpty()] [bool] $SkipApproval = $true, [switch] $Force, [ValidateNotNullOrEmpty()] [Personio.Core.AccessToken] $Token ) begin { } process { if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } $parameterSetName = $pscmdlet.ParameterSetName Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Attendance", "Remove" # fill pipedin query parameters if ($parameterSetName -like "ApiNative") { $id = $attendanceId } elseif ($parameterSetName -like "UserFriendly") { $id = $attendance.Id } # Prepare query $invokeParam = @{ "Type" = "DELETE" "ApiPath" = "company/attendances/$($id)" "Token" = $Token "QueryParameter" = @{ "skip_approval" = $SkipApproval.ToString().ToLower() } "AdditionalHeader" = @{ "accept" = "application/json" } } $processMessage = "attendance id '$($id)'" if ($parameterSetName -like "UserFriendly") { $processMessage = $processMessage + " (" + $attendance.Employee + " for " + (Get-Date -Date $attendance.Start -Format "HH:mm") + " - " + (Get-Date -Date $attendance.End -Format "HH:mm") + " on " + (Get-Date -Date $attendance.Start -Format "yyyy-MM-dd") + ")" } if (-not $Force) { if ($pscmdlet.ShouldProcess($processMessage, "Remove")) { $Force = $true } } if ($Force) { Write-PSFMessage -Level Verbose -Message "Remove $($processMessage)" -Tag "Attendance", "Remove" # Execute query $response = Invoke-PERSRequest @invokeParam # Check response and add to responseList if ($response.success) { Write-PSFMessage -Level Verbose -Message "Attendance id '$($id)' was removed. Message: $($response.data.message)" -Tag "Attendance", "Remove", "Result" } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "Attendance", "Remove", "Result" } } # Cleanup variable Remove-Variable -Name Token, id, doRemove, processMessage -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore } end { } } function Connect-Personio { <# .Synopsis Connect-Personio .DESCRIPTION Connect to Personio Service .PARAMETER Credential The access token as a credential object to login This is the recommended way to use the function, due to security reason. Username has to be the Client_ID from api access manament of Personio Password has to be the Client_Secret from api access manament of Personio .PARAMETER ClientId The Client_ID from api access manament of Personio Even if prodived as a logon method, due to best practices and security reason, you should consider to use the Credential parameter to connect! .PARAMETER ClientSecret The Client_Secret from api access manament of Personio Even if prodived as a logon method, due to best practices and security reason, you should consider to use the Credential parameter to connect! .PARAMETER URL Name of the service to connect to. Default is 'https://api.personio.de' as predefined value, but you can -for whatever reason- change the uri if needed. .PARAMETER APIVersion Version of API endpoint to use Default is 'V1' .PARAMETER PassThru Outputs the token to the console .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Connect-Personio -Credential (Get-Credential "ClientID") Connects to "api.personio.de" with the specified credentials. Connection will be set as default connection for any further action. .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [CmdletBinding( DefaultParameterSetName = 'Credential', SupportsShouldProcess = $false, PositionalBinding = $true, ConfirmImpact = 'Medium' )] Param( [Parameter( Mandatory = $true, ParameterSetName = 'Credential' )] [Alias("Token", "AccessToken", "APIToken")] [System.Management.Automation.PSCredential] $Credential, [Parameter( Mandatory = $true, ParameterSetName = 'PlainText' )] [Alias("Id")] [string] $ClientId, [Parameter( Mandatory = $true, ParameterSetName = 'PlainText' )] [Alias("Secret")] [string] $ClientSecret, [ValidateNotNullOrEmpty()] [Alias("ComputerName", "Hostname", "Host", "ServerName")] [uri] $URL = 'https://api.personio.de', [ValidateNotNullOrEmpty()] [Alias("Version")] [string] $APIVersion = "v1", [switch] $PassThru ) begin { } process { } end { # Variable preperation [uri]$uri = $URL.AbsoluteUri + $APIVersion.Trim('/') [string]$applicationIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.ApplicationIdentifier' -Fallback "PSPersonio" [string]$partnerIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.PartnerIdentifier' -Fallback "" # Security checks if ($PsCmdlet.ParameterSetName -eq 'PlainText') { Write-PSFMessage -Level Warning -Message "You use potential unsecure login method! Even if prodived as a logon method, due to best practices and security reason, you should consider to use the Credential parameter to connect. Please take care about security and try to avoid plain text credentials." -Tag "Connection", "New", "Security", "PlainText" } # Extrect credential if ($PsCmdlet.ParameterSetName -eq 'Credential') { [string]$ClientId = $Credential.UserName [string]$ClientSecret = $Credential.GetNetworkCredential().Password } # Invoke authentication Write-PSFMessage -Level Verbose -Message "Authenticate '$($ClientId)' as application '$($applicationIdentifier)' to service '$($uri.AbsoluteUri)'" -Tag "Connection", "Authentication", "New" $paramRestMethod = @{ "Uri" = "$($uri.AbsoluteUri)/auth?client_id=$($ClientId)&client_secret=$($ClientSecret)" "Headers" = @{ "X-Personio-Partner-ID" = $partnerIdentifier "X-Personio-App-ID" = $applicationIdentifier "accept" = "application/json" } "Method" = "POST" "Verbose" = $false "Debug" = $false "ErrorAction" = "Stop" "ErrorVariable" = "invokeError" } try { $response = Invoke-RestMethod @paramRestMethod } catch { Stop-PSFFunction -Message "Error invoking rest call on service '$($uri.AbsoluteUri)'. $($invokeError)" -Tag "Connection", "Authentication", "New" -EnableException $true -Cmdlet $pscmdlet } # Check response if ($response.success -notlike "True") { Stop-PSFFunction -Message "Service '$($uri.AbsoluteUri)' processes the authentication request, but response does not succeed" -Tag "Connection", "Authentication", "New" -EnableException $true -Cmdlet $pscmdlet } elseif (-not $response.data.token) { Stop-PSFFunction -Message "Something went wrong on authenticating user '$($ClientId)'. No token found in authentication respeonse. Unable login to service '$($uri.AbsoluteUri)'" -Tag "Connection", "Authentication", "New" -EnableException $true -Cmdlet $pscmdlet } else { Set-PSFConfig -Module 'PSPersonio' -Name 'API.URI' -Value $uri.AbsoluteUri } # Create output token Write-PSFMessage -Level Verbose -Message "Set Personio.Core.AccessToken" -Tag "Connection", "AccessToken", "New" $token = New-AccessToken -RawToken $response.data.token # Register AccessToken for further commands Register-AccessToken -Token $token Write-PSFMessage -Level Significant -Message "Connected to service '($($token.ApiUri))' with ClientId '$($token.ClientId)'. TokenId: $($token.TokenID) valid for $($token.AccessTokenLifeTime.toString())" -Tag "Connection" # Output if passthru if ($PassThru) { Write-PSFMessage -Level Verbose -Message "Output Personio.Core.AccessToken to console" -Tag "Connection", "AccessToken", "New" $token } # Cleanup Clear-Variable -Name paramRestMethod, uri, applicationIdentifier, partnerIdentifier, ClientId, ClientSecret, Credential, response, token -Force -WhatIf:$false -Confirm:$false -Debug:$false -Verbose:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore } } function Invoke-PERSRequest { <# .Synopsis Invoke-PersRequest .DESCRIPTION Basic function to invoke a API request to Personio service The function returns "raw data" from the API as a PSCustomObject. Titerally the function is a basic function within the core of the module. Most of the other functions, rely on Invoke-PresRequest to provide convenient data and functionality. .PARAMETER Type Type of web request .PARAMETER ApiPath Uri path for the REST call in the API .PARAMETER QueryParameter A hashtable for all the parameters to the api route .PARAMETER Body The body as a hashtable for the request .PARAMETER AdditionalHeader Additional headers to add in api call Provided as a hashtable .PARAMETER Token The TANSS.Connection token .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Invoke-PersRequest -Type GET -ApiPath "company/employees" Invoke a request to API route 'company/employees' as a GET call .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSTANSS #> [CmdletBinding( SupportsShouldProcess = $true, PositionalBinding = $true, ConfirmImpact = 'Medium' )] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateSet("GET", "POST", "PUT", "DELETE")] [string] $Type, [Parameter(Mandatory = $true)] [string] $ApiPath, [hashtable] $QueryParameter, [hashtable] $Body, [hashtable] $AdditionalHeader, [Personio.Core.AccessToken] $Token ) begin { } process { } end { #region Perpare variables # Check AccessToken if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } if (-not $Token) { Stop-PSFFunction -Message "No AccessToken found. Please connect to personio service frist. Use Connect-Personio command." -Tag "Connection", "MissingToken" -EnableException $true -Cmdlet $pscmdlet } if ($Token.IsValid) { Write-PSFMessage -Level System -Message "Valid AccessTokenId '$($Token.TokenID.ToString())' for service '$($Token.ApiUri)'." -Tag "WebRequest", "Token" } else { Stop-PSFFunction -Message "AccessTokenId '$($Token.TokenID.ToString())' is not valid. Please reconnect to personio service. Use Connect-Personio command." -Tag "Connection", "InvalidToken" -EnableException $true -Cmdlet $pscmdlet } # Get AppIds [string]$applicationIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.ApplicationIdentifier' -Fallback "PSPersonio" [string]$partnerIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.PartnerIdentifier' -Fallback "" # Format api path / api route to call $ApiPath = Format-ApiPath -Path $ApiPath -Token $Token -QueryParameter $QueryParameter # Format body if ($MyInvocation.BoundParameters['Body']) { $bodyData = $Body | ConvertTo-Json -Compress Write-PSFMessage -Level Debug -Message "BodyData: $($bodyData)" -Tag "WebRequest", "Body" } else { $bodyData = $null } # Format request header $header = @{ "Authorization" = "Bearer $([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token.Token)))" "X-Personio-Partner-ID" = $partnerIdentifier "X-Personio-App-ID" = $applicationIdentifier } if ($MyInvocation.BoundParameters['AdditionalHeader']) { foreach ($key in $AdditionalHeader.Keys) { $header.Add($key, $AdditionalHeader[$key]) } } #endregion Perpare variables # Invoke the api request to the personio service $paramInvoke = @{ "Uri" = "$($ApiPath)" "Headers" = $header "Body" = $bodyData "Method" = $Type "ContentType" = 'application/json; charset=UTF-8' "Verbose" = $false "Debug" = $false "ErrorAction" = "Stop" "ErrorVariable" = "invokeError" } if ($pscmdlet.ShouldProcess("$($Type) web REST call against URL '$($paramInvoke.Uri)'", "Invoke")) { Write-PSFMessage -Level Verbose -Message "Invoke $($Type) web REST call against URL '$($paramInvoke.Uri)'" -Tag "WebRequest", "Invoke" try { $response = Invoke-WebRequest @paramInvoke -UseBasicParsing $responseContent = $response.Content | ConvertFrom-Json Write-PSFMessage -Level System -Message "API Response: $($responseContent.success)" } catch { Write-PSFMessage -Level Error -Message "$($invokeError.Message) (StatusDescription:$($invokeError.ErrorRecord.Exception.Response.StatusDescription), Uri:$($ApiPath))" -Exception $invokeError.ErrorRecord.Exception -Tag "WebRequest", "Error", "API failure" -EnableException $true -PSCmdlet $pscmdlet return } # Create updated AccesToken from response. Every token can be used once and every api call will offer a new token Write-PSFMessage -Level System -Message "Update Personio.Core.AccessToken" -Tag "WebRequest", "Connection", "AccessToken", "Update" $token = New-AccessToken -RawToken $response.Headers['authorization'].Split(" ")[1] # Register updated AccessToken for further commands Register-AccessToken -Token $token Write-PSFMessage -Level Verbose -Message "Update AccessToken to Id '$($token.TokenID)'. Now valid up to $($token.TimeStampExpires.toString())" -Tag "WebRequest", "Connection", "AccessToken", "Update" # Check pagination if ($responseContent.metadata) { Write-PSFMessage -Level VeryVerbose -Message "Pagination detected! Retrieved records: $([Array]($responseContent.data).count) of $($responseContent.metadata.total_elements) total records (api call hast limit of $($responseContent.limit) records and started on record number $($responseContent.offset))" -Tag "WebRequest", "Pagination" } # Output data $responseContent } } } function Get-PERSEmployee { <# .Synopsis Get-PERSEmployee .DESCRIPTION List employee(s) from Personio The result can be paginated and. .PARAMETER InputObject Employee to call again It is inclusive, so the result starts from and including the provided StartDate .PARAMETER Email Find an employee with the given email address .PARAMETER UpdatedSince Find all employees that have been updated since the provided date NOTE: when using UpdatedSince, the Resultsize parameter is ignored .PARAMETER Attributes Define a list of whitelisted attributes that shall be returned for all employees .PARAMETER EmployeeId A list of Personio employee ID's to retrieve .PARAMETER ResultSize How much records will be returned from the api. Default is 200. Use this parameter, when function throw information about pagination .PARAMETER Token AccessToken object for Personio service .EXAMPLE PS C:\> Get-PERSEmployee Get all available company employees (api-side-pagination may kick in at 200) .NOTES Author: Andreas Bellstedt .LINK https://github.com/AndiBellstedt/PSPersonio #> [CmdletBinding( DefaultParameterSetName = "Default", SupportsShouldProcess = $false, PositionalBinding = $true, ConfirmImpact = 'Low' )] Param( [Parameter( ParameterSetName = "Default", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [string] $Email, [Parameter(ParameterSetName = "Default")] [datetime] $UpdatedSince, [Parameter(ParameterSetName = "Default")] [string[]] $Attributes, [Parameter(ParameterSetName = "Default")] [ValidateNotNullOrEmpty()] [int] $ResultSize, [Parameter( ParameterSetName = "ByType", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [Alias("Id", "EmployeeId")] [Personio.Employee.BasicEmployee[]] $InputObject, [ValidateNotNullOrEmpty()] [Personio.Core.AccessToken] $Token ) begin { # define script parameters $queryParameter = [ordered]@{} $typeNameBasic = "Personio.Employee.BasicEmployee" $typeNameExtended = "Personio.Employee.ExtendedEmployee" $memberNamesBasicEmployee = Expand-MemberNamesFromBasicObject -TypeName $typeNameBasic # fill query parameters if ($ResultSize) { $queryParameter.Add("limit", $ResultSize) $queryParameter.Add("offset", 0) } if ($UpdatedSince) { $queryParameter.Add("updated_since", (Get-Date -Date $UpdatedSince -Format "yyyy-MM-ddTHH:mm:ss")) } if ($attributes) { $queryParameter.Add("employees[]", $attributes) } } process { if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken } $parameterSetName = $pscmdlet.ParameterSetName Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Employee" # fill pipedin query parameters if ($Email) { $queryParameter.Add("email", $Email) } # Prepare query $invokeParam = @{ "Type" = "GET" "ApiPath" = "company/employees" "Token" = $Token } if ($queryParameter) { $invokeParam.Add("QueryParameter", $queryParameter) } # Execute query $responseList = [System.Collections.ArrayList]@() if ($parameterSetName -like "Default") { Write-PSFMessage -Level Verbose -Message "Getting available employees" -Tag "Employee", "Query" $response = Invoke-PERSRequest @invokeParam # Check respeonse and add to responeList if ($response.success) { $null = $responseList.Add($response) } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "Employee", "Query" } } elseif ($parameterSetName -like "ByType") { foreach ($inputItem in $InputObject) { Write-PSFMessage -Level Verbose -Message "Getting employee Id $($inputItem.Id)" -Tag "Employee", "Query" $invokeParam.ApiPath = "company/employees/$($inputItem.Id)" $response = Invoke-PERSRequest @invokeParam # Check respeonse and add to responeList if ($response.success) { $null = $responseList.Add($response) } else { Write-PSFMessage -Level Warning -Message "Personio api reported no data on employee Id $($inputItem.Id)" -Tag "Employee", "Query" } # remove token param for further api calls, due to the fact, that the passed in token, is no more valid after previous api all (api will use internal registered token) $invokeParam.Remove("Token") } } Remove-Variable -Name response -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore foreach ($response in $responseList) { # Check pagination / result limitation if ($response.metadata) { Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "Employee", "Query", "WebRequest", "Pagination" } # Process result $output = [System.Collections.ArrayList]@() foreach ($record in $response.data) { Write-PSFMessage -Level Debug -Message "Working on record Id $($record.attributes.id.value) name: $($record.attributes.first_name.value) $($record.attributes.last_name.value)" -Tag "Employee", "ObjectCreation" # Create object $result = New-Object -TypeName $typeNameBasic -Property @{ BaseObject = $record.attributes Id = $record.attributes.id.value Name = "$($record.attributes.last_name.value), $($record.attributes.first_name.value)" } $result.psobject.TypeNames.Insert(1, "Personio.Employee.$($record.type)") #region dynamic attribute checking and typeData Format generation $dynamicAttributeNames = $result.BaseObject.psobject.Properties.name | Where-Object { $_ -ne "id" -and $_ -NotIn $memberNamesBasicEmployee } $dynamicAttributes = $result.BaseObject.psobject.Members | Where-Object name -in $dynamicAttributeNames if ($dynamicAttributes) { # Dynamic attributes found Write-PSFMessage -Level Debug -Message "Employee with dynamic attribute ('$([string]::Join("', '", $dynamicAttributeNames))') detected, create [$($typeNameExtended)] object" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty" # Add sythetic type on top of employee object $result.psobject.TypeNames.Insert(0, $typeNameExtended) # Check synthetic typeData definition & compile the gathered dynamic properties if needed $typeExtendedEmployee = Get-TypeData -TypeName $typeNameExtended $_modified = $false foreach ($dynamicAttr in $dynamicAttributes) { #$label = $dynamicAttr.Value.label.split(" ") | ConvertTo-CamelCaseString # get property name $memberName = $dynamicAttr.Value.label.split(" ") | ConvertTo-CamelCaseString if (-not ($memberName -in $typeExtendedEmployee.Members.Keys)) { # dynamic property is new and currently not in TypeData defintion # check if property is a direct value or a PSObject if (($dynamicAttr.Value.value.psobject.TypeNames | Select-Object -First 1) -like "System.Management.Automation.PSCustomObject") { if ($dynamicAttr.Value.value.attributes.psobject.Properties.name -like "value") { [scriptblock]$value = [scriptblock]::Create( "`$this.BaseObject.$($dynamicAttr.Name).attributes.value" ) } elseif ($dynamicAttr.Value.value.attributes.psobject.Properties.name -like "name") { [scriptblock]$value = [scriptblock]::Create( "`$this.BaseObject.$($dynamicAttr.Name).attributes.name" ) } } else { [scriptblock]$value = [scriptblock]::Create( "`$this.BaseObject.$($dynamicAttr.Name).value" ) } Write-PSFMessage -Level Debug -Message "Add dynamic attribute '$($dynamicAttr.Name)' as property '$($memberName)' into TypeData for [$($typeNameExtended)]" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty", "TypeData" Update-TypeData -TypeName $typeNameExtended -MemberType ScriptProperty -MemberName $memberName -Value $value -Force $_modified = $true } } if ($_modified -or (-not (Get-FormatData -TypeName $typeNameExtended))) { Write-PSFMessage -Level Verbose -Message "New dynamic attributes within employee detected. TypeData for [Personio.Employee.ExtendedEmployee] was modified. Going to compile FormatData" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty", "FormatData" $typeBasicEmployee = Get-TypeData -TypeName $typeNameBasic $pathExtended = Join-Path -Path $env:TEMP -ChildPath "$($typeNameExtended).Format.ps1xml" $properties = @( "Id", "Name") $properties += $typeBasicEmployee.Members.Keys $properties += $dynamicAttributes.Value.label | ForEach-Object { $_.split(" ") | ConvertTo-CamelCaseString } #$typeExtendedEmployee.Members.Keys $properties = $properties | Where-Object { $_ -notlike 'SerializationData' } New-PS1XML -Path $pathExtended -TypeName $typeNameExtended -PropertyList $properties -View Table, List -Encoding UTF8 Write-PSFMessage -Level System -Message "Update FormatData with file '$($pathExtended)'" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty", "FormatData" Update-FormatData -PrependPath $pathExtended } } #endregion dynamic attribute checking and typeData Format generation # add objects to output array $null = $output.Add($result) } if ($output.Count -gt 1) { Write-PSFMessage -Level Verbose -Message "Retrieve $(([string]::Join(" & ", ($output | ForEach-Object { $_.psobject.TypeNames[0] } | Group-Object | ForEach-Object { "$($_.count) [$($_.Name)]" })))) objects" -Tag "Employee", "Result" } else { Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) object$(if($output) { " [" + $output[0].psobject.TypeNames[0] + "]"})" -Tag "Employee", "Result" } # Filtering #ToDo: Implement filtering for record output # output final results Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "Employee", "Result", "Output" foreach ($item in $output) { $item } } # Cleanup variable Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore $queryParameter.remove('email') } end { } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'PSPersonio' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> #region Module configurations Set-PSFConfig -Module 'PSPersonio' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'PSPersonio' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." Set-PSFConfig -Module 'PSPersonio' -Name 'WebClient.PartnerIdentifier' -Value "" -Initialize -Validation 'string' -Description "WebRequest header value - X-Personio-Partner-ID: The partner identifier" Set-PSFConfig -Module 'PSPersonio' -Name 'WebClient.ApplicationIdentifier' -Value "PSPersonio" -Initialize -Validation 'string' -Description "WebRequest header value - X-Personio-App-ID: The application identifier that integrates with Personio" Set-PSFConfig -Module 'PSPersonio' -Name 'API.URI' -Value "" -Initialize -Validation 'string' -Description "Base URI for API requests" #endregion Module configurations #region Module variables New-Variable -Name PersonioToken -Scope Script -Visibility Public -Description "Variable for registered access token. This is for convinience use with the commands in the module" -Force #endregion Module variables <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'PSPersonio.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "PSPersonio.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PSPersonio.alcohol #> New-PSFLicense -Product 'PSPersonio' -Manufacturer 'Andreas.Bellstedt' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2023-01-01") -Text @" Copyright (c) 2023 Andreas.Bellstedt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ #endregion Load compiled code |