WrikeExplorer.psm1
# GENERAL enum WrikeStatus { Scheduled InProgress Completed Cancelled Failed } # BI EXPORT class WrikeDataExportResource { [string]$Name [uri]$Url } class WrikeDataExport { [string]$Id [datetime]$CompletedDate [WrikeStatus]$Status [WrikeDataExportResource[]]$Resources } enum WrikeColumnDataType { Number String Date Boolean } class WrikeDataExportColumn { [string] $Id [string] $Alias [WrikeColumnDataType] $DataType [object] $ForeignKey } class WrikeDataExportSchema { [string] $Id [string] $Alias [WrikeDataExportColumn[]] $Columns } # CONTACTS enum WrikeUserType { Person Group } class WrikeUserProfile { [string] $AccountId [string] $Email [string] $Role [bool] $External [bool] $Admin [bool] $Owner } class WrikeContact { [string] $Id [string] $FirstName [string] $LastName [WrikeUserType] $Type [WrikeUserProfile[]] $Profiles [uri] $AvatarUrl [string] $TimeZone [string] $Locale [bool] $Deleted [bool] $Me [string[]] $MemberIds [object] $Metadata [bool] $MyTeam [string] $Title [string] $CompanyName [string] $Phone [string] $Location [string] $WorkScheduleId [object] $CurrentBillRate [object] $CurrentCostRate } enum WrikeRateSource { User JobRole } class WrikeBudgetRateChangeHistory { [WrikeRateSource] $RateSource [double] $RateValue [datetime] $StartDate [datetime] $EndDate } class WrikeContactHistory { [string] $Id [WrikeBudgetRateChangeHistory[]] $BillRateHistory [WrikeBudgetRateChangeHistory[]] $CostRateHistory } class WrikeErrorDescription { [string] $Error [string] $ErrorDescription } class WrikeApiVersion { [int] $Major [int] $Minor } enum WrikeProjectStatus { Green Yellow Red Completed OnHold Cancelled Custom } enum WrikeContractType { Billable NonBillable } class WrikeProject { [string] $AuthorId [string[]] $OwnerIds [WrikeProjectStatus] $Status [string] $CustomStatusId [datetime] $StartDate [datetime] $EndDate [datetime] $CreatedDate [datetime] $CompletedDate [WrikeContractType] $ContractType [object] $Finance } enum WrikeFolderTreeScope { WsRoot # Virtual root folder of account RbRoot # Virtual Recycle Bin folder of account WsFolder # Folder in account RbFolder # Folder is in Recycle Bin (deleted folder) WsTask # Task in account RbTask # Task is in Recycle Bin (deleted task) } enum WrikeColor { None Person Purple1 Purple2 Purple3 Purple4 Indigo1 Indigo2 Indigo3 Indigo4 DarkBlue1 DarkBlue2 DarkBlue3 DarkBlue4 Blue1 Blue2 Blue3 Blue4 Turquoise1 Turquoise2 Turquoise3 Turquoise4 DarkCyan1 DarkCyan2 DarkCyan3 DarkCyan4 Green1 Green2 Green3 Green4 YellowGreen1 YellowGreen2 YellowGreen3 YellowGreen4 Yellow1 Yellow2 Yellow3 Yellow4 Orange1 Orange2 Orange3 Orange4 Red1 Red2 Red3 Red4 Pink1 Pink2 Pink3 Pink4 Gray1 Gray2 Gray3 } class WrikeFolderTree { [string] $Id [string] $Title [string] $Color [string[]] $ChildIds [WrikeFolderTreeScope] $Scope [WrikeProject] $Project [bool] $Space } class WrikeFolder { [string] $Id [string] $AccountId [string] $Title [datetime] $CreatedDate [datetime] $UpdatedDate [string] $BriefDescription [string] $Description [WrikeColor] $Color [string[]] $SharedIds [string[]] $ParentIds [string[]] $ChildIds [string[]] $SuperParentIds [WrikeFolderTreeScope] $Scope [bool] $HasAttachments [int] $AttachmentCount [string] $Permalink [string] $WorkflowId [object[]] $Metadata [object[]] $CustomFields [string[]] $CustomColumnIds [WrikeProject] $Project } enum WrikeFieldComparator { EqualTo IsEmpty IsNotEmpty LessThan LessOrEqualTo GreaterThan GreaterOrEqualTo InRange NotInRange Contains StartsWith EndsWith ContainsAll ContainsAny } class WrikeFieldFilter { [string] $Id [WrikeFieldComparator] $Comparator [string[]] $Value [string] $MinValue [string] $MaxValue [string] ToString() { $obj = [ordered]@{ id = $this.Id; comparator = $this.Comparator.ToString() } if ($null -ne $this.Value -and $this.Value.Count -gt 0) { if ($this.Value.Count -eq 1) { $obj.value = $this.Value[0] } else { $obj.values = $this.Value } } else { if (![string]::IsNullOrWhiteSpace($this.MinValue)) { $obj.minValue = $this.MinValue } if (![string]::IsNullOrWhiteSpace($this.MaxValue)) { $obj.maxValue = $this.MaxValue } } return ($obj | ConvertTo-Json -Compress) } } enum WrikeCurrency { USD EUR GBP RUB BRL AED ARS BYR CAD CLP COP CZK DKK HKD HUF INR IDR ILS JPY KRW MYR MXN NZD NOK PEN PHP PLN QAR RON SAR SGD ZAR SEK CHF TWD THB TRY UAH VND CNY AUD AMD BWP } enum WrikeAggregationType { None Sum Average } enum WrikeCustomFieldType { Text DropDown Numeric Currency Percentage Date Duration Checkbox Contacts Multiple } class WrikeFieldType { static [WrikeCustomFieldType[]] $Comparable = @( [WrikeCustomFieldType]::Text, [WrikeCustomFieldType]::DropDown, [WrikeCustomFieldType]::Numeric, [WrikeCustomFieldType]::Currency, [WrikeCustomFieldType]::Percentage, [WrikeCustomFieldType]::Date, [WrikeCustomFieldType]::Duration ) static [WrikeCustomFieldType[]] $String = @( [WrikeCustomFieldType]::Text, [WrikeCustomFieldType]::DropDown ) static [WrikeCustomFieldType[]] $Collection = @( [WrikeCustomFieldType]::Contacts, [WrikeCustomFieldType]::Multiple ) static [WrikeCustomFieldType[]] $Boolean = @( [WrikeCustomFieldType]::Checkbox ) } class WrikeCustomFieldSettings { [string] $InheritanceType [int] $DecimalPlaces [bool] $UseThousandsSeparator [WrikeCurrency] $Currency [WrikeAggregationType] $Aggregation [string[]] $Values [bool] $AllowOtherValues [string[]] $Contacts [bool] $ReadOnly [bool] $AllowTime } class WrikeCustomField { [string] $Id [string] $AccountId [string] $Title [WrikeCustomFieldType] $Type [string] $SpaceId [string[]] $SharedIds [WrikeCustomFieldSettings] $Settings } class WrikeProjectHistory { [object[]] $ActualFees [object[]] $ActualCost [object[]] $PlannedFees [object[]] $PlannedCost [object[]] $Budget } class WrikeFolderHistory { [string] $Id [WrikeProjectHistory] $Project } function ConvertFrom-WrikeErrorDescription { [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [WrikeErrorDescription] $WrikeErrorDescription ) process { $categoryMap = @{ invalid_request = 'ProtocolError' invalid_parameter = 'InvalidArgument' parameter_required = 'InvalidData' not_authorized = 'SecurityError' access_forbidden = 'PermissionDenied' not_allowed = 'PermissionDenied' resource_not_found = 'ObjectNotFound' method_not_found = 'InvalidOperation' too_many_requests = 'LimitsExceeded' rate_limit_exceeded = 'LimitsExceeded' server_error = 'NotSpecified' } $e = [exception]::new($WrikeErrorDescription.ErrorDescription) $category = [System.Management.Automation.ErrorCategory]::($categoryMap.($WrikeErrorDescription.Error)) $record = [System.Management.Automation.ErrorRecord]::new($e, $WrikeErrorDescription.Error, $category, $null) Write-Output $record } } function ConvertTo-PlainText { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [securestring] $Secret ) process { Write-Output ([pscredential]::new('a', $Secret).GetNetworkCredential().Password) } } function ConvertTo-TimestampInterval { [CmdletBinding()] param ( [Parameter()] [Nullable[datetime]] $Start, [Parameter()] [Nullable[datetime]] $End, [Parameter()] [switch] $AsJson ) process { if ($null -ne $Start -and $null -ne $End -and $End -le $Start) { Write-Error 'Timestamp for end cannot be less than or equal to timestamp for start.' return } $interval = @{} if ($MyInvocation.BoundParameters.ContainsKey('Start') -and $null -ne $Start) { $interval.start = $Start.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } if ($MyInvocation.BoundParameters.ContainsKey('End') -and $null -ne $End) { $interval.end = $End.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } if ($AsJson) { $interval | ConvertTo-Json -Compress } else { Write-Output $interval } } } function Get-StandardHeaders { [CmdletBinding()] param () process { $headers = @{ Authorization = "Bearer $($script:Config.Wrike.AccessToken | ConvertTo-PlainText)" } Write-Output $headers } } function Import-AccessToken { [CmdletBinding()] param () process { $path = Join-Path $script:Config.AppDataPath 'accesstoken.txt' if (-not (Test-Path -Path $path)) { return } try { $token = Get-Content -Path $path | ConvertTo-SecureString $script:Config.Wrike.AccessToken = $token } catch { Write-Error "Failed to import Wrike API App access token from '$path'. Consider deleting the file and using Set-WrikeAccessToken again." } } } function Remove-AccessToken { [CmdletBinding()] param ( ) process { $script:Config.Wrike.AccessToken = $null $path = Join-Path $script:Config.AppDataPath 'accesstoken.txt' Remove-Item -Path $path -Force } } function Save-AccessToken { [CmdletBinding()] param ( # Specifies the API App access token to use for authentication [Parameter(Mandatory)] [securestring] $AccessToken, # Specifies that the token should not be persisted to disk [Parameter()] [switch] $Ephemeral ) process { $script:Config.Wrike.AccessToken = $AccessToken if (-not $Ephemeral) { $path = Join-Path $script:Config.AppDataPath 'accesstoken.txt' $AccessToken | ConvertFrom-SecureString | Set-Content -Path $path } } } function ValidateAccessToken { [CmdletBinding()] param () process { if ([string]::IsNullOrWhiteSpace($script:Config.Wrike.AccessToken)) { throw "Wrike API App access token has not been set. Please use Set-WrikeAccessToken." } } } function Clear-WrikeAccessToken { [CmdletBinding(SupportsShouldProcess)] param ( ) process { if ($PSCmdlet.ShouldProcess("Wrike API Access Token", "Clear")) { Remove-AccessToken } } } function Get-WrikeApiVersion { [CmdletBinding()] param ( ) process { Invoke-WrikeApi -Path version -ResponseType version } } function Get-WrikeContact { [CmdletBinding()] [OutputType([WrikeContact])] param ( # Specifies one or more optional ID values for known contacts [Parameter(ValueFromPipelineByPropertyName)] [ValidateCount(0, 100)] [ValidateNotNullOrEmpty()] [string[]] $Id, # Specifies that the Wrike Contact record for the current user should be returned [Parameter()] [Alias('CurrentUser')] [switch] $Me, # Specifies that only deleted Wrike Contact records should be returned [Parameter()] [switch] $Deleted, # Specifies a set of optional fields to be included in the Wrike Contact response model. Normally these values are empty. [Parameter()] [ValidateSet('metadata', 'workScheduleId', 'currentBillRate', 'currentCostRate', IgnoreCase = $false)] [string[]] $Include ) process { $query = @{} if ($MyInvocation.BoundParameters.ContainsKey('Me')) { $query.me = $Me.ToString().ToLower() } if ($MyInvocation.BoundParameters.ContainsKey('Deleted')) { $query.deleted = $Deleted.ToString().ToLower() } if ($MyInvocation.BoundParameters.ContainsKey('Include')) { # De-duplicate the list of optional fields $Include = $Include | Group-Object | Select-Object -ExpandProperty Name $json = $Include | ConvertTo-Json -Compress if ($Include.Count -lt 2) { $json = "[$json]" } $query.fields = $json } $path = "contacts" if ($null -ne $Id -and $Id.Count -gt 0) { $path += '/' + [string]::Join(',', $Id) } Invoke-WrikeApi -Path $path -ResponseType contacts -Query $query } } function Get-WrikeContactHistory { [CmdletBinding()] [OutputType([WrikeContactHistory])] param ( # Specifies one or more optional ID values for known contacts [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateCount(1, 100)] [ValidateNotNullOrEmpty()] [string[]] $Id, # Specifies the inclusive start of the time range for the contact history request [Parameter()] [datetime] $UpdatedAfter, # Specifies the inclusive end of the time range for the contact history request [Parameter()] [datetime] $UpdatedBefore, # Specifies a set of optional fields to be included in the Wrike Contact response model. Normally these values are empty. [Parameter()] [ValidateSet('billRate', 'costRate', IgnoreCase = $false)] [string[]] $Include ) process { if ($null -ne $UpdatedAfter -and $null -ne $UpdatedBefore -and $UpdatedBefore -le $UpdatedAfter) { Write-Error 'UpdatedBefore cannot be less than or equal to UpdatedAfter.' return } $query = @{} if ($MyInvocation.BoundParameters.ContainsKey('Include')) { # De-duplicate the list of optional fields $Include = $Include | Group-Object | Select-Object -ExpandProperty Name $json = $Include | ConvertTo-Json -Compress if ($Include.Count -lt 2) { $json = "[$json]" } $query.fields = $json } if ($MyInvocation.BoundParameters.ContainsKey('UpdatedAfter') -or $MyInvocation.BoundParameters.ContainsKey('UpdatedBefore')) { $query.updatedDate = ConvertTo-TimestampInterval -Start $UpdatedAfter -End $UpdatedBefore -AsJson } $path = 'contacts' if ($null -ne $Id -and $Id.Count -gt 0) { $path += '/' + [string]::Join(',', $Id) } $path += '/contacts_history' Invoke-WrikeApi -Path $path -ResponseType contactsHistory -Query $query } } function Get-WrikeCustomField { [CmdletBinding()] [OutputType([WrikeCustomField])] param ( # Specifies one or more optional id's to limit the results. [Parameter(ValueFromPipelineByPropertyName)] [ValidateCount(0, 100)] [string[]] $Id ) process { $path = 'customfields' if ($null -ne $Id -and $Id.Count -gt 0) { $path += '/' + [string]::Join(',', $Id) } Invoke-WrikeApi -Path $path -ResponseType 'customfields' } } function Get-WrikeDataExport { [CmdletBinding()] [OutputType([WrikeDataExport])] param() process { Invoke-WrikeApi -Path 'data_export' -ResponseType dataExport } } function Get-WrikeDataExportSchema { [CmdletBinding()] param () process { Invoke-WrikeApi -Path 'data_export_schema' -ResponseType dataExportSchema } } function Get-WrikeExplorerConfig { [CmdletBinding()] param ( ) process { Write-Output $script:Config } } function Get-WrikeFolder { [CmdletBinding()] param ( # Specifies one or more optional folder ID values for known folders. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string[]] $Id, # Specifies the permalink url for the folder [Parameter(ValueFromPipelineByPropertyName)] [string] $Permalink, # Specifies a metadata filter key [Parameter()] [ValidateLength(1, 50)] [ValidatePattern('[A-Za-z0-9_-]+')] [string] $Key, # Specifies a metadata filter value [Parameter()] [ValidateLength(0,1000)] [string] $Value, # Specifies an optional custom field filter to limit the search scope. Use New-WrikeFieldFilter to compose a filter. [Parameter()] [WrikeFieldFilter] $FieldFilter, # Specifies which type of records to return, projects, folders, or all. Default is 'All'. [Parameter()] [ValidateSet('Projects', 'Folders', 'All')] [string] $Type = 'All', # Specifies the inclusive last-updated time. Only records last updated after this time will be included in the search scope. [Parameter()] [datetime] $UpdatedAfter, # Specifies the inclusive last-updated time. Only records last updated before this time will be included in the search scope. [Parameter()] [datetime] $UpdatedBefore, # Specifies that only deleted records should be returned. [Parameter()] [switch] $Deleted, # Specifies that descendant folders should not be included in the search scope. [Parameter()] [switch] $NoDescendants, # Specifies one or more contract types to include in the search scope. [Parameter()] [WrikeContractType[]] $ContractType, # Specifies a set of optional fields to be included in the response model. [Parameter()] [ValidateSet('metadata', 'hasAttachments', 'attachmentCount', 'description', 'briefDescription', 'customFields', 'customColumnIds', 'superParentIds', 'space', 'contractType', IgnoreCase = $false)] [string[]] $Include ) process { if ($Id.Count -gt 100) { Write-Error 'The Wrike API imposes a limit of 100 IDs in a query.' return } if ($null -ne $UpdatedAfter -and $null -ne $UpdatedBefore -and $UpdatedBefore -le $UpdatedAfter) { Write-Error 'UpdatedBefore cannot be less than or equal to UpdatedAfter.' return } $query = @{} if ($MyInvocation.BoundParameters.ContainsKey('Permalink')) { $query.permalink = $Permalink } if ($MyInvocation.BoundParameters.ContainsKey('NoDescendants')) { $query.descendants = (!$NoDescendants).ToString().ToLower() } if ($MyInvocation.BoundParameters.ContainsKey('Key')) { $query.metadata = @{ key = $Key; value = $Value } | ConvertTo-Json -Compress } if ($MyInvocation.BoundParameters.ContainsKey('FieldFilter')) { $query.customField = $FieldFilter.ToString() } if ($MyInvocation.BoundParameters.ContainsKey('UpdatedAfter') -or $MyInvocation.BoundParameters.ContainsKey('UpdatedBefore')) { $query.updatedDate = ConvertTo-TimestampInterval -Start $UpdatedAfter -End $UpdatedBefore -AsJson } if ($Type -ne 'All') { $query.project = ($Type -eq 'Project').ToString().ToLower() } if ($MyInvocation.BoundParameters.ContainsKey('Deleted')) { $query.deleted = $Deleted.ToString().ToLower() } if ($MyInvocation.BoundParameters.ContainsKey('ContractType')) { $selectedTypes = $ContractType | Foreach-Object { $_.ToString() } | Group-Object | Select-Object -ExpandProperty Name $json = $selectedTypes | ConvertTo-Json -Compress if ($selectedTypes.Count -lt 2) { $json = "[$json]" } $query.contractTypes = $json } if ($MyInvocation.BoundParameters.ContainsKey('Include')) { $Include = $Include | Group-Object | Select-Object -ExpandProperty Name $json = $Include | ConvertTo-Json -Compress if ($Include.Count -lt 2) { $json = "[$json]" } $query.fields = $json } $path = 'folders' $responseType = 'folderTree' if ($null -ne $Id -and $Id.Count -gt 0) { $path += '/' + [string]::Join(',', $Id) $responseType = 'folders' } if (($query.Keys | Where-Object { $_ -in @('permalink', 'descendants', 'metadata', 'customField', 'updatedDate', 'project', 'contractTypes') })) { $responseType = 'folders' } Invoke-WrikeApi -Path $path -ResponseType $responseType -Query $query } } function Get-WrikeFolderHistory { [CmdletBinding()] [OutputType([WrikeFolderHistory])] param ( # Specifies one or more ID's for known records records. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [ValidateCount(1, 100)] [string[]] $Id, # Specifies the inclusive start of the time range for the contact history request [Parameter()] [datetime] $UpdatedAfter, # Specifies the inclusive end of the time range for the contact history request [Parameter()] [datetime] $UpdatedBefore, # Specifies a set of optional fields to be included in the Wrike Contact response model. Normally these values are empty. [Parameter()] [ValidateSet('actualFees', 'actualCost', 'plannedFees', 'plannedCost', 'budget', IgnoreCase = $false)] [string[]] $Include ) process { $query = @{} if ($MyInvocation.BoundParameters.ContainsKey('Include')) { $Include = $Include | Group-Object | Select-Object -ExpandProperty Name $json = $Include | ConvertTo-Json -Compress if ($Include.Count -lt 2) { $json = "[$json]" } $query.fields = $json } if ($MyInvocation.BoundParameters.ContainsKey('UpdatedAfter') -or $MyInvocation.BoundParameters.ContainsKey('UpdatedBefore')) { $query.updatedDate = ConvertTo-TimestampInterval -Start $UpdatedAfter -End $UpdatedBefore -AsJson } $path = 'folders' if ($null -ne $Id -and $Id.Count -gt 0) { $path += '/' + [string]::Join(',', $Id) } $path += '/folders_history' Invoke-WrikeApi -Path $path -ResponseType foldersHistory -Query $query } } function Invoke-WrikeApi { [CmdletBinding()] param ( # Specifies the HTTP method to use for the API call [Parameter()] [ValidateSet('Get', 'Post', 'Delete', 'Put', 'Patch', 'Head', 'Options', 'Merge', 'Trace', 'Default')] [string] $Method = 'Get', # Specifies the full, or base uri to use for the Wrike API call. By default this will be the BaseUri from the module's configuration file. [Parameter()] [Uri] $Uri = [uri]$script:Config.Wrike.BaseUri, # Specifies the relative path of the Wrike API to call [Parameter()] [string] $Path = [string]::Empty, # Specifies the query string keys and values if applicable [Parameter()] [hashtable] $Query = @{}, # Specifies any additional headers needed. The Authorization header will be added for you, and if you supply it manually it will be overwritten. [Parameter()] [hashtable] $Headers = @{}, # Specifies the body for the API call if applicable [Parameter()] [object] $Body, # Specifies the path to save files for Wrike API calls that are intended to download data [Parameter()] [string] $OutFile, # Specifies the expected response type. This determines the class used to parse the results into a strong type. [Parameter()] [string] $ResponseType ) begin { ValidateAccessToken } process { if ($MyInvocation.BoundParameters.ContainsKey('ResponseType') -and -not $script:ResponseType.ContainsKey($ResponseType)) { throw "Unexpected ResponseType passed to Invoke-WrikeApi: $ResponseType" } $Path = $Path.TrimStart('/') $uriBuilder = [uribuilder]([uri]::new($Uri, $Path)) $queryStringCollection = [System.Web.HttpUtility]::ParseQueryString($uriBuilder.Query) $Query.Keys | ForEach-Object { $queryStringCollection.Add($_, $Query.$_) } $uriBuilder.Query = $queryStringCollection.ToString() $standardHeaders = Get-StandardHeaders $standardHeaders.Keys | ForEach-Object { $Headers.$_ = $standardHeaders.$_ } $requestParams = @{ Method = $Method Uri = $uriBuilder.Uri Headers = $Headers } foreach ($p in 'Body', 'OutFile') { if ($MyInvocation.BoundParameters.ContainsKey($p)) { Write-Verbose "Adding parameter '$p' with value '$($MyInvocation.BoundParameters[$p])' to Invoke-RestMethod invocation " $requestParams.$p = $MyInvocation.BoundParameters[$p] } } try { $response = Invoke-RestMethod @requestParams Write-Verbose "Processing API response with kind -eq '$($response.kind)'" if ($MyInvocation.BoundParameters.ContainsKey('ResponseType')) { if ($response.kind -eq $ResponseType) { $dataType = $script:ResponseType.$ResponseType foreach ($data in $response.data) { $obj = $dataType::new() foreach ($property in $data | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) { $obj.$property = $data.$property } Write-Output $obj } } else { Write-Error "Unexpected response from Wrike api: $($response | ConvertTo-Json -Depth 10)" } } elseif ($response) { Write-Output $response } } catch [System.Net.Http.HttpRequestException], [System.InvalidOperationException] { $errorRecord = $_ Write-Verbose "Handling $($errorRecord.Exception.GetType()) exception" if ($errorDescription = ($errorRecord.ErrorDetails.Message | ConvertFrom-Json -Depth 10) -as [WrikeErrorDescription]) { Write-Verbose "Successfully parsed Wrike API error response" $errorRecord = $errorDescription | ConvertFrom-WrikeErrorDescription } Write-Error -ErrorRecord $errorRecord } } } function New-WrikeFieldFilter { [CmdletBinding()] [OutputType([WrikeFieldFilter])] param ( # Specifies the custom field ID. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Id, # Specifies the comparison operator. [Parameter()] [WrikeFieldComparator] $Comparator, # Specifies the filter value, or optionally a set of values. [Parameter()] [string[]] $Value, # Specifies the minimum value for comparable field types. [Parameter()] [string] $MinValue, # Specifies the maximum value for comparable field types. [Parameter()] [string] $MaxValue, # Specifies that the filter should be returned even if the ID is not recognized as an existing Wrike custom field ID. [Parameter()] [switch] $Force ) process { if (-not $Force) { $customField = Get-WrikeCustomField -Id $Id -ErrorAction Stop Write-Verbose "Creating field filter for field named '$($customField.Title)'" $compatibleComparators = @{ [WrikeCustomFieldType]::Text = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange, [WrikeFieldComparator]::Contains, [WrikeFieldComparator]::StartsWith, [WrikeFieldComparator]::EndsWith [WrikeCustomFieldType]::DropDown = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange, [WrikeFieldComparator]::Contains, [WrikeFieldComparator]::StartsWith, [WrikeFieldComparator]::EndsWith [WrikeCustomFieldType]::Numeric = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange [WrikeCustomFieldType]::Currency = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange [WrikeCustomFieldType]::Percentage = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange [WrikeCustomFieldType]::Date = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange [WrikeCustomFieldType]::Duration = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::LessThan, [WrikeFieldComparator]::LessOrEqualTo, [WrikeFieldComparator]::GreaterThan, [WrikeFieldComparator]::GreaterOrEqualTo, [WrikeFieldComparator]::InRange, [WrikeFieldComparator]::NotInRange [WrikeCustomFieldType]::Checkbox = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty [WrikeCustomFieldType]::Contacts = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::ContainsAll, [WrikeFieldComparator]::ContainsAny [WrikeCustomFieldType]::Multiple = [WrikeFieldComparator]::EqualTo, [WrikeFieldComparator]::IsEmpty, [WrikeFieldComparator]::IsNotEmpty, [WrikeFieldComparator]::ContainsAll, [WrikeFieldComparator]::ContainsAny } if ($Comparator -notin $compatibleComparators.($customField.Type)) { Write-Error "The chosen comparator '$Comparator' is not valid for use with the custom field '$($customField.Title)' of type '$($customField.Type)'" return } } [WrikeFieldFilter]@{ Id = $Id Comparator = $Comparator Value = $Value MinValue = $MinValue MaxValue = $MaxValue } } } function Request-WrikeDataExport { [CmdletBinding()] [OutputType([WrikeDataExport])] param( # Specifies the maximum age of the BI Export data before a refresh request is issued. [Parameter()] [TimeSpan] $MaxAge = ( New-TimeSpan -Hours 12 ), # Specifies that if a new BI Export request is issued, the command should block until the request is completed and the results are returned [Parameter()] [switch] $Wait, # Specifies the maximum time to wait for a result. [Parameter()] [TimeSpan] $Timeout = [TimeSpan]::MaxValue ) process { $minAge = New-TimeSpan -Hours 1 if ($MaxAge -lt $minAge) { Write-Verbose "MaxAge is less than 1 hour. MaxAge will be set to 1 hour." $MaxAge = $minAge } $data = Get-WrikeDataExport $now = if ($data.CompletedDate.Kind -eq [System.DateTimeKind]::Utc) { (Get-Date).ToUniversalTime() } else { Get-Date } if ($null -ne $data -and $now - $data.CompletedDate -le $MaxAge) { # The last report is not older than MaxAge so we just return it Write-Output $data return } # Either there is no BI Export data available or it's older than MaxAge Write-Verbose "Requesting updated BI Export data" $now = Get-Date $expireTime = if ($Timeout -eq [TimeSpan]::MaxValue) { [datetime]::MaxValue } else { $now.Add($Timeout) } $data = Invoke-WrikeApi -Method Post -Path 'data_export' -ResponseType dataExport if (-not $Wait) { # Return early with a Status of Scheduled or InProgress probably Write-Output $data return } Write-Verbose "Waiting until $expireTime" $completedStates = @([WrikeStatus]::Completed, [WrikeStatus]::Cancelled, [WrikeStatus]::Failed) $requestInterval = New-TimeSpan -Seconds $script:Config.Wrike.RequestIntervalSeconds while ($data.Status -notin $completedStates -and (Get-Date) -lt $expireTime) { Write-Verbose "BI Export status is $($data.Status). Checking again in $requestInterval. . ." Start-Sleep -Seconds $requestInterval.TotalSeconds $data = Request-WrikeDataExport -MaxAge $MaxAge } Write-Output $data } } function Save-WrikeDataExport { [CmdletBinding()] param( # Specifies the WrikeDataExport data returned from Get-WrikeDataExport or Request-WrikeDataExport. If ommitted, a call will be made to Get-WrikeDataExport automatically. [Parameter(ValueFromPipeline)] [WrikeDataExport] $WrikeDataExport, # Specifies the directory where all Wrike BI Export resources will be written as CSV files. Path will be created if it doesn't exist, and existing files will be overwritten. [Parameter(Mandatory)] [string] $Destination ) process { $null = New-Item -Path $Destination -ItemType Directory -Force -ErrorAction Stop if ($null -eq $WrikeDataExport) { $WrikeDataExport = Get-WrikeDataExport if ($null -eq $WrikeDataExport) { Write-Error "There is no Wrike BI Export data available. Please use Request-WrikeDataExport to refresh the BI Export data." return } } foreach ($resource in $WrikeDataExport.Resources) { $requestParams = @{ Uri = $resource.Url OutFile = Join-Path $Destination "$($resource.Name).csv" } $ProgressPreference = 'SilentlyContinue' Invoke-WrikeApi @requestParams } } } function Set-WrikeAccessToken { [CmdletBinding(SupportsShouldProcess)] param ( # Specifies the API App access token to use for authentication. It is recommended to provide a securestring, but a string can also be provided. [Parameter(Mandatory)] [object] $AccessToken, # Specifies that the token should not be persisted to disk [Parameter()] [switch] $Ephemeral ) process { $valueType = $AccessToken.GetType().Name if ($valueType -eq 'String') { Write-Warning "The AccessToken value was received as a string. For better security it is recommended to use a securestring instead. Otherwise it's possible your PowerShell command history could be read, and secrets like these could be stolen. Try using 'Read-Host -AsSecureString' to collect tokens, passwords, and secrets in the future." Write-Verbose 'Converting the AccessToken string to a securestring' $AccessToken = $AccessToken | ConvertTo-SecureString -AsPlainText -Force } elseif ($valueType -ne 'SecureString') { Write-Error "AccessToken is expected to be of type SecureString or String. Received a $valueType instead." } if ($PSCmdlet.ShouldProcess("Wrike API Access Token", "Set")) { Write-Verbose "Setting the Wrike API access token. Ephemeral = $Ephemeral" Save-AccessToken -AccessToken $AccessToken -Ephemeral:$Ephemeral } } } $script:Config = Import-PowerShellDataFile $PSScriptRoot\config.psd1 if ($script:Config.AppDataPath -eq 'Default') { $script:Config.AppDataPath = Join-Path -Path ([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ApplicationData)) -ChildPath 'WrikeExplorer' } if (-not (Test-Path $script:Config.AppDataPath)) { New-Item -Path $script:Config.AppDataPath -ItemType Directory } $script:ResponseType = @{ contacts = [WrikeContact] dataExport = [WrikeDataExport] dataExportSchema = [WrikeDataExportSchema] contactsHistory = [WrikeContactHistory] version = [WrikeApiVersion] folderTree = [WrikeFolderTree] folders = [WrikeFolder] customfields = [WrikeCustomField] foldersHistory = [WrikeFolderHistory] } Import-AccessToken |