Public/Invoke-NqlExport.ps1

function Invoke-NqlExport {
    <#
.SYNOPSIS
    Executes a Nexthink NQL export and downloads the result file.
 
.DESCRIPTION
    Submits an NQL query export request to Nexthink, polls the export status
    until completion or error, then downloads the resulting file to the
    specified output folder.
 
    This function:
      - Validates the NQL QueryId format.
      - Submits an export request with an optional compression mode.
      - Polls the export status endpoint until:
          • status = COMPLETED → downloads the file
          • status = ERROR → throws with the error description
      - Derives the remote file name from the returned URL.
      - Downloads the file into the given OutputFolder.
      - Warns if the resulting file is zero bytes.
      - Returns a FileInfo object for the downloaded file.
 
.PARAMETER QueryId
    The NQL Query Identifier to export.
 
    Example:
        "#my_nql_export_query"
 
.PARAMETER Parameters
    Optional hashtable of key/value pairs representing query parameters.
    Keys must match the parameter names defined inside the target query.
 
.PARAMETER Compression
    The compression mode for the exported data.
 
    Valid values:
        NONE - No compression
        GZIP - GZIP-compressed output
        ZSTD - Zstandard-compressed output
 
    Default:
        NONE
 
.PARAMETER OutputFolder
    The directory where the exported file will be downloaded.
 
    Requirements:
      - Must exist and be a directory.
      - Must be writable by the current user.
 
    Validation:
      - Uses Test-WritableFolder -Path <OutputFolder> to ensure write access.
 
.INPUTS
    None. This function does not accept pipeline input.
 
.OUTPUTS
    [System.IO.FileInfo]
 
    Returns a FileInfo object representing the downloaded export file.
 
.EXAMPLE
    Invoke-NqlExport `
        -QueryId "#my_nql_export_query" `
        -OutputFolder "C:\Exports"
 
    Executes the NQL export, polls until completion, and downloads the result
    into C:\Exports. Returns the FileInfo for the downloaded file.
 
.EXAMPLE
    $params = @{
        fromDate = "2025-11-01"
        toDate = "2025-11-30"
    }
 
    Invoke-NqlExport `
        -QueryId "#audit_events_export" `
        -Parameters $params `
        -Compression GZIP `
        -OutputFolder "C:\Exports\Logs"
 
    Executes a parameterized NQL export with GZIP compression and downloads
    the resulting file into C:\Exports\Logs.
 
.NOTES
    - The export request is submitted to the Nexthink NQL export endpoint and
      polled every 5 seconds until a terminal status is reached.
    - Nexthink-side limits (row count, execution time) still apply to the
      underlying query.
#>

    [CmdletBinding()]
    param(
        [ValidatePattern('^#[A-z0-9_]{2,255}$')]
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$QueryId,

        [parameter(Mandatory = $false)]
        [hashtable]$Parameters,

        [ValidateSet('NONE', 'GZIP', 'ZSTD', IgnoreCase = $true)]
        [parameter(Mandatory = $false)]
        [string]$Compression = 'NONE',

        [parameter(Mandatory = $true)]
        [ValidateScript({
                Test-WritableFolder -Path $_ | Out-Null
                $true
            })]
        [System.IO.DirectoryInfo]$OutputFolder
    )
    $APITYPE = 'NQL_Export'

    ## Build the body for the NQL Query execution
    $body = @{
        queryId     = $QueryId
        compression = $Compression
    }
    # add any optional dynamic parameters
    if (($null -ne $Parameters) -and ($Parameters.count -ge 1)) {
        $body.Add('parameters', $Parameters)
    }
    $bodyJson = $body | ConvertTo-Json -Depth 4

    ## Make initial API Call to get Export ID
    $exportIdResponse = Invoke-NxtApi -Type $APITYPE -Body $bodyJson -ReturnResponse

    if ($null -eq $exportIdResponse.exportId) {
        throw "No exportId returned from NQL Export request"
    }
    $exportId = $exportIdResponse.exportId
    Write-CustomLog -Message "ExportId: $exportId" -Severity "DEBUG"

    ## Now Get the Export Status and Download Link
    $APITYPE = 'NQL_Export_Status'

    while ($true) {
        Start-Sleep -Seconds 5
        $statusResponse = Invoke-NxtApi -Type $APITYPE -Query $exportId -ReturnResponse

        if ($statusResponse.status -eq 'COMPLETED') {
            $resultsFileUrl = $statusResponse.resultsFileUrl
            Write-CustomLog -Message "Results File URL: $resultsFileUrl" -Severity "DEBUG"

            $remoteFileName = [IO.Path]::GetFileName( ([uri]$resultsFileUrl).AbsolutePath )
            $outputFile = [System.IO.FileInfo](Join-Path -Path $OutputFolder -ChildPath $remoteFileName)

            break
        }
        elseif ($statusResponse.status -eq 'ERROR') {
            Write-CustomLog -Message "NQL Export returned an Error Status" -Severity "ERROR"
            throw "NQL Export Error: $($statusResponse.errorDescription)"
        }
        else {
            Write-CustomLog -Message "Export Status: $($statusResponse.status). Waiting..." -Severity "DEBUG"
        }
    }

    # Now download the file
    try {
        Write-CustomLog -Message "Downloading output to $($outputFile.Name)" -Severity "DEBUG"
        Invoke-RestMethod -Uri $resultsFileUrl -Method 'GET' -OutFile $outputFile
    }
    catch {
        Write-CustomLog -Message "Error downloading NQL Export file: $_" -Severity "ERROR"
        throw $_
    }
    # Invoke-RestMethod -Uri $resultsFileUrl -Method 'GET' | Out-File -FilePath $outputFile

    # Check the size of the output file and send a warning if it is zero bytes
    if ($outputFile.Length -eq 0) {
        Write-CustomLog -Message "Warning: NQL Export file is zero bytes: $($outputFile.FullName)" -Severity "WARNING"
    }

    return $outputFile
}