src/Client/Invoke-XrmCustomApiLoop.ps1

<#
    .SYNOPSIS
    Call a custom API repeatedly, while a condition holds, up to a ceiling.

    .DESCRIPTION
    Call the custom API (see Invoke-XrmCustomApi) until the While scriptblock returns false or MaxIterations calls were made: the "process by batches until nothing is left" pattern.
    The While and OnIteration scriptblocks receive the output of the last call in $_ (the response, or the deserialized JSON with JsonOutput), and the output and the iteration number as arguments.
    OnIteration output goes to the host. A failed call stops the loop with an error.
    Returns Iterations, LastOutput and CeilingReached (true when the loop stopped on MaxIterations while While still returned true).

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (Dataverse ServiceClient)

    .PARAMETER Name
    Custom API unique name (message name).

    .PARAMETER Parameters
    Request parameters, by name, sent at each call. (Default: none)

    .PARAMETER While
    Condition evaluated after each call: the loop goes on while it returns true (e.g. { $_.HasMore }).

    .PARAMETER MaxIterations
    Maximum number of calls. (Default: 100)

    .PARAMETER OnIteration
    Scriptblock run after each call, before While (e.g. to log progress).

    .PARAMETER JsonOutput
    Name of an output parameter holding JSON: its deserialized value is given to the scriptblocks and returned as LastOutput.

    .OUTPUTS
    PSCustomObject. Iterations, LastOutput, CeilingReached.

    .EXAMPLE
    $result = Invoke-XrmCustomApiLoop -XrmClient $xrmClient -Name "contoso_PurgeLogs" -Parameters @{ BatchSize = 500 } -JsonOutput "Result" `
        -While { $_.HasMore } -MaxIterations 200 -OnIteration { param($output, $iteration) Write-Host "Batch $iteration : $($output.Deleted) deleted" };
    if ($result.CeilingReached) { Write-Warning "Stopped after $($result.Iterations) batches, logs remain."; }

    .LINK
    https://github.com/AymericM78/PowerDataOps/blob/main/documentation/commands/Invoke-XrmCustomApiLoop.md
#>

function Invoke-XrmCustomApiLoop {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [Hashtable]
        $Parameters,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ScriptBlock]
        $While,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $MaxIterations = 100,

        [Parameter(Mandatory = $false)]
        [ScriptBlock]
        $OnIteration,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $JsonOutput
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        $callParameters = @{ XrmClient = $XrmClient; Name = $Name; ErrorAction = "Stop" };
        if ($PSBoundParameters.ContainsKey('Parameters')) {
            $callParameters["Parameters"] = $Parameters;
        }
        if ($PSBoundParameters.ContainsKey('JsonOutput')) {
            $callParameters["JsonOutput"] = $JsonOutput;
        }

        $iteration = 0;
        $output = $null;
        $continue = $true;
        while ($continue -and $iteration -lt $MaxIterations) {
            $iteration++;
            $output = Invoke-XrmCustomApi @callParameters;
            if ($WhatIfPreference -and $null -eq $output) {
                # Call skipped: nothing to evaluate
                $continue = $false;
                break;
            }

            $context = [System.Collections.Generic.List[psvariable]]::new();
            $context.Add([psvariable]::new("_", $output));
            if ($OnIteration) {
                $OnIteration.InvokeWithContext($null, $context, @($output, $iteration)) | Out-Host;
            }
            $condition = $While.InvokeWithContext($null, $context, @($output, $iteration));
            $continue = [bool]($condition | Select-Object -Last 1);
        }

        [PSCustomObject]@{
            Iterations     = $iteration;
            LastOutput     = $output;
            CeilingReached = $continue -and $iteration -ge $MaxIterations;
        };
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Invoke-XrmCustomApiLoop -Alias *;