private/Invoke-VSAWriteRequest.ps1

function Invoke-VSAWriteRequest {
    <#
    .SYNOPSIS
       Shared dispatch tail for every write cmdlet (New/Update/Set/Enable/... POST/PUT/PATCH/DELETE).
    .DESCRIPTION
       Absorbs the boilerplate that used to be hand-copied at the end of ~100 public write cmdlets:
       body pruning, JSON serialization, request-parameter assembly, connection forwarding, and the
       optional ExtendedOutput expansion. Centralizing it removes two whole bug classes that came
       from divergent hand-rolled copies:

         * F-31 -- a cmdlet forgetting `if ($VSAConnection) { $Params.VSAConnection = ... }`, so an
           explicit -VSAConnection was silently ignored. The forwarding now happens in exactly one
           place.
         * F-52 -- pruning a body with `-not $BodyHT[$key]` (truthiness), which drops a legitimate
           0, $false, or '' the caller meant to send. This prunes ONLY $null and empty-string, so
           an explicit 0 / $false survives.

       Body handling by type of -Body:
         * [hashtable]/[IDictionary] : pruned (unless -KeepEmpty) then serialized to compact JSON at
           -Depth (default 10, deep enough for the module's most nested bodies -- ContactInfo,
           CustomFields, LicenseValues -- which the old per-cmdlet default depth of 2 silently
           truncated).
         * [string] : passed through unchanged (already-serialized JSON).
         * absent / $null : no request body (e.g. a templated-URI PUT like Set-VSAAgentName).

       ExtendedOutput mirrors the historical New-VSAOrganization/Department/MachineGroup contract
       exactly: the switch is forwarded to the transport (which returns the full response envelope)
       and the caller-visible result is then $response.Result via Select-Object -ExpandProperty.
    .PARAMETER Body
        The request body: a hashtable (serialized here), a pre-serialized JSON string, or omitted.
    .PARAMETER Method
        The HTTP method (POST/PUT/PATCH/DELETE).
    .PARAMETER URISuffix
        The endpoint URI suffix (already formatted with any path segments by the caller).
    .PARAMETER VSAConnection
        The explicit connection; forwarded to the transport only when non-null (else the persistent
        connection is used).
    .PARAMETER ExtendedOutput
        When set, returns the expanded Result from the full response envelope (create-cmdlet contract).
    .PARAMETER KeepEmpty
        Skips body pruning; use when a cmdlet must transmit explicit null/empty-string fields.
    .PARAMETER Depth
        ConvertTo-Json depth for hashtable bodies (default 10).
    .OUTPUTS
        Whatever the transport returns for the request (bool status, created id, envelope, or $null).
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory = $false)]
        [AllowNull()]
        [object] $Body,

        [parameter(Mandatory = $true)]
        [ValidateSet('POST', 'PUT', 'PATCH', 'DELETE')]
        [string] $Method,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $URISuffix,

        [parameter(Mandatory = $false)]
        [AllowNull()]
        [VSAConnection] $VSAConnection,

        [parameter(Mandatory = $false)]
        [switch] $ExtendedOutput,

        [parameter(Mandatory = $false)]
        [switch] $KeepEmpty,

        [parameter(Mandatory = $false)]
        [int] $Depth = 10
    )

    [hashtable] $Params = @{
        URISuffix = $URISuffix
        Method    = $Method
    }

    # Resolve the body: serialize a hashtable, pass a string through, or send none.
    if ($Body -is [System.Collections.IDictionary]) {
        [hashtable] $BodyHT = @{}
        foreach ($key in $Body.Keys) { $BodyHT[$key] = $Body[$key] }

        if (-not $KeepEmpty) {
            # Prune ONLY $null / empty-string. A legitimate 0 or $false is preserved (F-52).
            foreach ($key in @($BodyHT.Keys)) {
                $value = $BodyHT[$key]
                if ($null -eq $value -or ($value -is [string] -and $value.Length -eq 0)) {
                    $BodyHT.Remove($key)
                }
            }
        }

        $Params['Body'] = $BodyHT | ConvertTo-Json -Depth $Depth -Compress
    }
    elseif ($Body -is [string] -and -not [string]::IsNullOrEmpty($Body)) {
        $Params['Body'] = $Body
    }

    # Forward the explicit connection only when present (F-31): the transport falls back to the
    # persistent connection when it is absent.
    if ($VSAConnection) { $Params['VSAConnection'] = $VSAConnection }

    Write-Debug ("Invoke-VSAWriteRequest. {0} {1}`n{2}" -f $Method, $URISuffix, ($Params | Out-String))

    $Result = Invoke-VSARestMethod @Params -ExtendedOutput:$ExtendedOutput

    # Historical create-cmdlet contract: with -ExtendedOutput the transport returns the envelope,
    # from which the caller expects the expanded .Result.
    if ($ExtendedOutput) {
        $Result = $Result | Select-Object -ExpandProperty Result
    }

    return $Result
}