Public/Activities/Get-JIMActivity.ps1

# Copyright (c) Tetron Limited. All rights reserved.
# Licensed under the Tetron Commercial License. See LICENSE file in the project root.

function Get-JIMActivity {
    <#
    .SYNOPSIS
        Gets Activities from JIM.
 
    .DESCRIPTION
        Retrieves activity history from JIM. Activities track all operations performed
        in JIM, including sync runs, data generation, certificate management, and other
        administrative actions.
 
    .PARAMETER Id
        The Activity to get.
 
    .PARAMETER Search
        Return only the Activities whose target name or type contains this text; "Active Directory", for example,
        finds the Activities of that Connected System.
 
    .PARAMETER Page
        The page of Activities to return.
 
    .PARAMETER PageSize
        How many Activities per page.
 
    .PARAMETER Operation
        Return only the Activities that performed one of these operations, for example Execute for a Run Profile
        execution, or Receive for the requests a SCIM client sent to a Connected System's SCIM endpoint.
 
    .PARAMETER Outcome
        Return only the Activities that recorded at least one of these outcomes, for example Errors for runs that
        recorded an error.
 
    .PARAMETER Status
        Return only the Activities with one of these statuses.
 
    .PARAMETER InitiatedById
        Return only the Activities initiated by this principal (a Metaverse Object or API key). -InitiatedBy matches
        on the recorded name instead.
 
    .PARAMETER InitiatedBy
        Return only the Activities whose initiator's recorded name contains this text, case-insensitively.
 
    .PARAMETER HasChildActivities
        Return only the Activities that have child Activities ($true), or only those that have none ($false). Omit
        it to return both.
 
    .PARAMETER CreatedFrom
        Return only the Activities created at or after this time, which is converted to UTC.
 
    .PARAMETER CreatedTo
        Return only the Activities created at or before this time, which is converted to UTC.
 
    .PARAMETER ConnectedSystem
        Return only the Activities of these Connected Systems, by name, ignoring case.
 
    .PARAMETER RunProfile
        Return only the executions of these Run Profiles, by name, ignoring case.
 
    .PARAMETER ScheduledOnly
        Return only the Activities a Schedule produced ($true), or only those nobody scheduled ($false). Omit it to
        return both.
 
    .PARAMETER ScheduleId
        Return only the Activities these Schedules produced.
 
    .PARAMETER ExecutionItems
        Return the Activity's Run Profile Execution Items: what happened to each object.
 
    .PARAMETER Follow
        Follow the Activity's progress until it finishes, like tail -f, showing the step it is on, object counts,
        throughput and the estimated time remaining. Ctrl+C stops following early.
 
    .PARAMETER IntervalSeconds
        How many seconds to wait between reads of the progress.
 
    .PARAMETER MaxPolls
        How many times to read the progress before following stops, whether or not the Activity has finished, for a
        script that must not wait indefinitely. Omit it to follow until the Activity finishes.
 
    .OUTPUTS
        A list returns each Activity's header, and -Id the Activity in full. -ExecutionItems returns each of the
        Activity's Run Profile Execution Items. -Follow shows progress while it follows, then returns the Activity
        in full.
 
    .EXAMPLE
        Get-JIMActivity
 
        Gets the most recent activities.
 
    .EXAMPLE
        Get-JIMActivity -Page 2 -PageSize 50
 
        Gets page 2 of activities with 50 items per page.
 
    .EXAMPLE
        Get-JIMActivity -Search "Full Import"
 
        Gets activities matching "Full Import".
 
    .EXAMPLE
        Get-JIMActivity -Status FailedWithError, CompleteWithError
 
        Gets the activities that failed or completed with errors.
 
    .EXAMPLE
        Get-JIMActivity -ConnectedSystem 'Contoso AD' -RunProfile 'Full Import' -Status FailedWithError
 
        Gets the failed Full Import runs against the 'Contoso AD' Connected System.
 
    .EXAMPLE
        Get-JIMActivity -ScheduledOnly $true -Outcome Errors -CreatedFrom (Get-Date).AddDays(-7)
 
        Gets the last week's scheduled work that recorded errors, which answers whether a Schedule has been
        failing repeatedly or only once.
 
    .EXAMPLE
        Get-JIMSchedule -Name 'Nightly Sync' | ForEach-Object { Get-JIMActivity -ScheduleId $_.Id }
 
        Gets the Activities the 'Nightly Sync' Schedule produced.
 
    .EXAMPLE
        Get-JIMActivity -InitiatedBy 'alice' -CreatedFrom (Get-Date).AddDays(-30)
 
        Gets the last 30 days of activities initiated by anyone whose name contains "alice".
 
    .EXAMPLE
        Get-JIMActivity -Id "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
 
        Gets a specific activity by ID.
 
    .EXAMPLE
        Get-JIMActivity -Id "a1b2c3d4-e5f6-7890-abcd-ef1234567890" -ExecutionItems
 
        Gets the execution items for a specific run profile activity.
 
    .EXAMPLE
        Get-JIMActivity -Id "a1b2c3d4-e5f6-7890-abcd-ef1234567890" -Follow
 
        Follows the Activity's live progress until it completes, then returns the final Activity.
 
    .EXAMPLE
        Get-JIMActivity -Id "a1b2c3d4-e5f6-7890-abcd-ef1234567890" -Follow -IntervalSeconds 5 -MaxPolls 60
 
        Follows the Activity's progress for at most 5 minutes, polling every 5 seconds.
 
    .LINK
        Get-JIMActivityStats
        Start-JIMRunProfile
    #>

    [CmdletBinding(DefaultParameterSetName = 'List')]
    [OutputType('JIM.ActivityHeader', ParameterSetName = 'List')]
    [OutputType('JIM.ActivityDetailDto', ParameterSetName = 'ById')]
    [OutputType('JIM.ActivityDetailDto', ParameterSetName = 'Follow')]
    [OutputType('JIM.ActivityRunProfileExecutionItemHeader', ParameterSetName = 'ExecutionItems')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ById', ValueFromPipelineByPropertyName)]
        [Parameter(Mandatory, ParameterSetName = 'ExecutionItems', ValueFromPipelineByPropertyName)]
        [Parameter(Mandatory, ParameterSetName = 'Follow', ValueFromPipelineByPropertyName)]
        [guid]$Id,

        [Parameter(ParameterSetName = 'List')]
        [string]$Search,

        [Parameter(ParameterSetName = 'List')]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$Page = 1,

        [Parameter(ParameterSetName = 'List')]
        [ValidateRange(1, 100)]
        [int]$PageSize = 20,

        [Parameter(ParameterSetName = 'List')]
        [ValidateSet('Create', 'Read', 'Update', 'Delete', 'Clear', 'Execute', 'ImportHierarchy', 'ImportSchema',
            'Revert', 'Reset', 'Authenticate', 'Preview', 'Receive')]
        [string[]]$Operation,

        [Parameter(ParameterSetName = 'List')]
        [ValidateSet('Added', 'Updated', 'Deleted', 'Projected', 'Joined', 'AttributeFlows', 'Disconnected',
            'DriftCorrections', 'Provisioned', 'Exported', 'Deprovisioned', 'Created', 'PendingExports', 'Errors')]
        [string[]]$Outcome,

        [Parameter(ParameterSetName = 'List')]
        [ValidateSet('InProgress', 'Complete', 'CompleteWithWarning', 'CompleteWithError', 'FailedWithError', 'Cancelled')]
        [string[]]$Status,

        [Parameter(ParameterSetName = 'List')]
        [guid]$InitiatedById,

        [Parameter(ParameterSetName = 'List')]
        [string]$InitiatedBy,

        [Parameter(ParameterSetName = 'List')]
        [bool]$HasChildActivities,

        [Parameter(ParameterSetName = 'List')]
        [datetime]$CreatedFrom,

        [Parameter(ParameterSetName = 'List')]
        [datetime]$CreatedTo,

        [Parameter(ParameterSetName = 'List')]
        [string[]]$ConnectedSystem,

        [Parameter(ParameterSetName = 'List')]
        [string[]]$RunProfile,

        [Parameter(ParameterSetName = 'List')]
        [bool]$ScheduledOnly,

        [Parameter(ParameterSetName = 'List')]
        [guid[]]$ScheduleId,

        [Parameter(Mandatory, ParameterSetName = 'ExecutionItems')]
        [switch]$ExecutionItems,

        [Parameter(Mandatory, ParameterSetName = 'Follow')]
        [switch]$Follow,

        [Parameter(ParameterSetName = 'Follow')]
        [ValidateRange(1, 300)]
        [int]$IntervalSeconds = 2,

        [Parameter(ParameterSetName = 'Follow')]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$MaxPolls
    )

    process {
        # Check connection first
        if (-not $script:JIMConnection) {
            Write-Error "You are not connected to JIM. Run Connect-JIM -Url <your JIM URL> to authenticate, then try again."
            return
        }

        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                Write-Verbose "Getting Activity with ID: $Id"
                $result = Invoke-JIMApi -Endpoint "/api/v1/activities/$Id"
                $result
            }

            'Follow' {
                Write-Verbose "Following progress for Activity ID: $Id"
                $pollCount = 0
                $hasMaxPolls = $PSBoundParameters.ContainsKey('MaxPolls')
                $lastMessage = ''
                $terminalStatuses = @('Complete', 'CompleteWithWarning', 'CompleteWithError', 'FailedWithError', 'Cancelled')
                $terminal = $false

                while ($true) {
                    $progress = $null
                    try {
                        $progress = Invoke-JIMApi -Endpoint "/api/v1/activities/$Id/progress"
                    }
                    catch {
                        # Transient failures should not end the follow; the next poll retries.
                        Write-Warning "Error reading Activity progress: $_"
                    }
                    $pollCount++

                    if ($progress) {
                        $message = "$($progress.message ?? '')"
                        if ($message -and $message -ne $lastMessage) {
                            Write-Verbose $message
                            $lastMessage = $message
                        }

                        $progressParams = Get-JIMActivityProgressDisplay -Progress $progress -ActivityLabel 'Following Activity'
                        Write-Progress @progressParams

                        if ("$($progress.status)" -in $terminalStatuses) {
                            $terminal = $true
                        }
                    }

                    if ($terminal) {
                        break
                    }

                    if ($hasMaxPolls -and $pollCount -ge $MaxPolls) {
                        Write-Verbose "Stopped following after $pollCount polls (MaxPolls reached); the Activity may still be running."
                        break
                    }

                    Start-Sleep -Seconds $IntervalSeconds
                }

                Write-Progress -Activity 'Following Activity' -Completed

                # Emit the final Activity so callers receive the same shape as -Id.
                Invoke-JIMApi -Endpoint "/api/v1/activities/$Id"
            }

            'ExecutionItems' {
                Write-Verbose "Getting execution items for Activity ID: $Id"
                $currentPage = 1
                $hasMore = $true

                while ($hasMore) {
                    $response = Invoke-JIMApi -Endpoint "/api/v1/activities/$Id/items?page=$currentPage&pageSize=100"

                    # Stream items individually
                    foreach ($item in $response.items) {
                        $item
                    }

                    # Check if there are more pages
                    $hasMore = ($response.items.Count -eq 100) -and ($currentPage * 100 -lt $response.totalCount)
                    $currentPage++
                }
            }

            'List' {
                Write-Verbose "Getting Activities (Page: $Page, PageSize: $PageSize)"

                # The filters are evaluated by the API, which narrows the list through the same Activity
                # query the portal uses, so both surfaces return the same Activities for the same filters.
                # Multi-valued filters repeat their query parameter, one entry per value.
                $queryParams = @("page=$Page", "pageSize=$PageSize")

                if ($Search) {
                    $queryParams += "search=$([System.Uri]::EscapeDataString($Search))"
                }

                foreach ($operationValue in $Operation) {
                    $queryParams += "operation=$operationValue"
                }

                foreach ($outcomeValue in $Outcome) {
                    $queryParams += "outcome=$outcomeValue"
                }

                foreach ($statusValue in $Status) {
                    $queryParams += "status=$statusValue"
                }

                if ($PSBoundParameters.ContainsKey('InitiatedById')) {
                    $queryParams += "initiatedById=$InitiatedById"
                }

                if ($InitiatedBy) {
                    $queryParams += "initiatedBy=$([System.Uri]::EscapeDataString($InitiatedBy))"
                }

                # Bound-parameter checks rather than truthiness: $false is a meaningful value for these two
                # ("only Activities without children", "only unscheduled work"), and would otherwise be
                # indistinguishable from the parameter having been omitted.
                if ($PSBoundParameters.ContainsKey('HasChildActivities')) {
                    $queryParams += "hasChildActivities=$($HasChildActivities.ToString().ToLowerInvariant())"
                }

                if ($PSBoundParameters.ContainsKey('CreatedFrom')) {
                    $queryParams += "createdFrom=$($CreatedFrom.ToUniversalTime().ToString('o'))"
                }

                if ($PSBoundParameters.ContainsKey('CreatedTo')) {
                    $queryParams += "createdTo=$($CreatedTo.ToUniversalTime().ToString('o'))"
                }

                foreach ($connectedSystemValue in $ConnectedSystem) {
                    $queryParams += "connectedSystem=$([System.Uri]::EscapeDataString($connectedSystemValue))"
                }

                foreach ($runProfileValue in $RunProfile) {
                    $queryParams += "runProfile=$([System.Uri]::EscapeDataString($runProfileValue))"
                }

                if ($PSBoundParameters.ContainsKey('ScheduledOnly')) {
                    $queryParams += "scheduledOnly=$($ScheduledOnly.ToString().ToLowerInvariant())"
                }

                foreach ($scheduleIdValue in $ScheduleId) {
                    $queryParams += "scheduleId=$scheduleIdValue"
                }

                $endpoint = "/api/v1/activities?$($queryParams -join '&')"

                $response = Invoke-JIMApi -Endpoint $endpoint

                # Output each activity individually for pipeline support
                foreach ($activity in $response.items) {
                    $activity
                }

                # Add pagination info to verbose output
                Write-Verbose "Returned $($response.items.Count) of $($response.totalCount) total activities"
            }
        }
    }
}