PSIdoitNG.psm1

#Region '.\Private\Convert-PropertyToArray.ps1' -1

function Convert-PropertyToArray {
<#
    .SYNOPSIS
    Convert-PropertyToArray
 
    .DESCRIPTION
    This function converts a PowerShell object with properties into an array of objects.
 
    .PARAMETER InputObject
    The input object to be converted. Details see example.
 
    .OUTPUTS
    Returns an array of objects, each containing the property name and value.
 
    .EXAMPLE
    Convert-PropertyToArray -InputObject $inputObject
    [PSCustomObject] @{
        C__OBJTYPE__SERVICE = 'System Service';
        C__OBJTYPE__APPLICATION = 'Application';
        ...
    }
    into an array of the following definition
    [PSCustomObject] @{
        Name = 'C__OBJTYPE__SERVICE';
        Value = 'System Service';
    }
        .... and so on
#>

    [CmdletBinding()]
    param (
        [PSCustomObject]$InputObject
    )
    $result = foreach ($property in $InputObject.PSObject.Properties) {
        [PSCustomObject]@{
            Name  = $property.Name
            Value = $property.Value
        }
    }
    return $result
}
#EndRegion '.\Private\Convert-PropertyToArray.ps1' 41
#Region '.\Private\ConvertFrom-CustomCategory.ps1' -1

function ConvertFrom-CustomCategory {
    <#
    .SYNOPSIS
    Converts a custom category object into a more user-friendly format.
 
    .DESCRIPTION
    ConvertFrom-CustomCategory gives a more user-friendly representation of a custom category object.
    It retrieves category information and formats the properties into a custom object with more readable property names.
    The PSCustom property names are derived from the category information titles, cleaning up any non-alphanumeric characters.
 
    .PARAMETER InputObject
    The input object to convert.
 
    .PARAMETER CategoryObject
    The category object containing the category information to use for conversion.
 
    .EXAMPLE
    $inputObject = Get-IdoitObject -Id 12345
    $objTypeCatList = Get-IdoitObjectTypeCategory -ObjId $Id -ErrorAction Stop | Where-Object { $_.const -eq 'C__CATG__CUSTOM_FIELDS_COMPONENT' }
    $newObject = ConvertFrom-CustomCategory -InputObject $inputObject -CategoryObject $objTypeCatList
 
    This would be an explicit example of how to use the ConvertFrom-CustomCategory function.
 
    .EXAMPLE
    $inputObject = Get-IdoitObject -Id 12345
    $newObject = Get-IdoitCategory -Id 12345 -Category 'C__CATG__CUSTOM_FIELDS_COMPONENT'
 
    This is an example, where ConvertFrom-CustomCategory is used implicitly by Get-IdoitCategory.
 
    .NOTES
    The converted names get all non-alphanumeric characters replaced with an underscore.
    If the category information is not available, the original property names from the input object are retained.
    Category properties that are only for UI purposes (like 'hr' or 'html') are skipped.
 
    To make Set-IdoitCategory work easier, the original property names are stored in a special property called 'psid_custom'.
    The 'psid_custom' property is a hashtable that maps the new property names to the original property names.
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSObject]$InputObject,

        [Parameter(Mandatory = $true)]
        [PSObject] $CategoryObject
    )

    begin {

    }

    process {
        $catInfo = Get-IdoitCategoryInfo -Category $CategoryObject.const -ErrorAction Stop
        $result = [ordered]@{
            objId = $InputObject.objId
            id    = $InputObject.id
            psid_custom = @{}             # indicates that this is a modified custom category object, stores the names
        }
        foreach ($property in $catInfo.PSObject.Properties) {
            if ($catInfo.PSObject.Properties[$property.Name]) {
                $newPropName = $property.Value.title -replace '[^a-zA-Z0-9]', '_'
                # If the property seems to be just for UI purposes, we will skip it
                if ($property.Value.ui.type -in 'hr','html') {
                    continue
                }
                if ('' -eq $newPropName) {
                    continue                        # this seems to happen for the category name?
                }
                # $InputObject | Add-Member -MemberType NoteProperty -Name $property.Name -Value $catInfo.PSObject.Properties[$property.Name].Value -Force
                $result.Add($newPropName,  $InputObject.($property.name).Title)
                $result.psid_custom.Add($newPropName, $property.Name) # store the original name for later reference
            } else {
                # If the property is not defined in the category info, we will add it as a NoteProperty with value from InputObject
                $result.Add($property.Name, $InputObject[$property.Name])
                $result.psid_custom.Add($property.Name, $property.Name)
            }
        }
        [PSCustomObject] $result
    }

    end {

    }
}
#EndRegion '.\Private\ConvertFrom-CustomCategory.ps1' 85
#Region '.\Private\ModuleStartup.ps1' -1

function ModuleStartup {
    <#
    .SYNOPSIS
        Initializes the module by setting up the script parameters.
 
    .DESCRIPTION
        This function is called when the module is loaded. It sets up the script parameters and initializes the module.
 
    .EXAMPLE
        ModuleStartup
        This will initialize the module and set up the script parameters.
 
    .NOTES
        The function will be inserted into the .psm1 as any other function.
        At the end of the file, the function will be called to initialize the module.
        So the code will be executed when the module is loaded.
    #>

    [CmdletBinding()]
    param()

    $Script:IdoItParams = @{}
    $Script:IdoItParams['Connection'] = @{
        Uri       = $null
        Username  = $null
        Password  = $null
        ApiKey    = $null
        SessionId = $null
    }
}
ModuleStartup
#EndRegion '.\Private\ModuleStartup.ps1' 31
#Region '.\Private\NewDynamicParameter.ps1' -1

function NewDynamicParameter {
    <#
    .SYNOPSIS
    Create a new dynamic parameter.
 
    .DESCRIPTION
    This function creates a new dynamic parameter with the specified name, parameter set name, and attributes.
 
    .PARAMETER Name
    The name of the dynamic parameter.
 
    .PARAMETER ParametersetName
    The name of the parameter set to which the dynamic parameter belongs.
 
    .PARAMETER Mandatory
    Indicates whether the dynamic parameter is mandatory.
 
    .PARAMETER ParameterType
    The type of the dynamic parameter.
 
    .PARAMETER ValidateSet
    An array of valid values for the dynamic parameter.
    If not provided, the parameter will not have a validation set.
 
    .OUTPUTS
    Returns a RuntimeDefinedParameter object representing the dynamic parameter.
 
    .EXAMPLE
    $dynParam = NewDynamicParameter -Name 'MyDynamicParam' -ParametersetName 'MyParameterSet' -Mandatory $true -ParameterType 'System.String' -ValidateSet 'Value1', 'Value2'
    This example creates a new dynamic parameter named 'MyDynamicParam' with the specified attributes.
 
    .NOTES
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.RuntimeDefinedParameter])]
    param (
        [Parameter(Mandatory = $true)]
        [string] $Name,

        [string] $ParametersetName = '',

        [boolean] $Mandatory = $false,

        [string] $ParameterType = 'System.String',

        [string[]] $ValidateSet = $null
    )

    $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

    $attribute = New-Object System.Management.Automation.ParameterAttribute
    $attribute.Mandatory = $Mandatory
    $attribute.ParameterSetName = $ParametersetName
    $attributeCollection.Add($attribute)

    if ($null -ne $ValidateSet) {
        $attribute = New-Object System.Management.Automation.ValidateSetAttribute($ValidateSet)
        $attributeCollection.Add($attribute)
    }

    $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, $ParameterType, $attributeCollection)
    Write-Output $dynParam
}
#EndRegion '.\Private\NewDynamicParameter.ps1' 64
#Region '.\Private\ValidateProperties.ps1' -1

function ValidateProperties {
    <#
    .SYNOPSIS
    Validates properties for a given category.
 
    .DESCRIPTION
    This function checks if the provided properties are valid for the specified category in i-doit.
    It validates the properties against the category's information and returns a hashtable of valid properties.
    If any property is invalid, it issues a warning and removes that property from the returned hashtable.
    Reason to validate locally?
    Field type dialog_plus would create a new entry if was not defined in the category.
    To avoid accidental creation of new entries because of a typo, we validate the properties first.
 
    .PARAMETER Category
    The category to validate properties against. This should be a valid i-doit category identifier.
 
    .PARAMETER Properties
    A hashtable of properties to validate. The keys should be property names and the values should be the corresponding values.
 
    .PARAMETER ExcludeProperties
    An array of property names to exclude from validation. These properties will be included in the returned hashtable without validation.
 
    .EXAMPLE
    $properties = @{
        'f_popup_c_17289168067044910' = 'Job / Schnittstelle' # custom field; select one from a list
        'f_popup_c_17289128195752470' = @('SQL Server','Biztalk') # custom field; multiselect
    }
    $validProperties = ValidateProperties -Category 'C__CATG__CUSTOM_FIELDS_KOMPONENTE' -Properties $properties
    This example validates the properties for the category 'C__CATG__CUSTOM_FIELDS_KOMPONENTE' and returns a hashtable of valid properties.
 
    .NOTES
    #>

    [CmdletBinding()]
    [OutputType([Hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [string] $Category,

        [Parameter(Mandatory = $true)]
        [hashtable] $Properties,

        [string[]] $ExcludeProperties = @(
            'objID', 'id', 'psid_custom', 'entry', 'Category'
        )
    )

    $atLeastOneFailed = $false
    $validProperties = @{}
    $catInfo = Get-IdoitCategoryInfo -Category $Category -ErrorAction Stop
    foreach ($key in $Properties.Keys) {
        if ($ExcludeProperties -contains $key) {
            $validProperties[$key] = $Properties[$key]
            Write-Verbose "Excluding property '$key' from validation for category '$($Category)'"
            continue
        }
        if ($null -eq $catInfo.$key) {
            Write-Warning "Property '$key' is not a valid property for category '$($Category)'. Skipping it."
            continue
        }
        if ($catInfo.$key.data.readonly) {
            $Properties.Remove($key)
            continue
        }
        Write-Verbose "Validating property '$key' for category '$($Category)'"
        switch  -Regex ($catInfo.$key.info.type) {
            'dialog' {         # type "dialog", "dialog_plus"
                if ($null -eq $Properties[$key]) {
                    continue
                }
                $thisDialogOptions = Get-IdoitDialog -params @{ category = $Category; property = $key } -ErrorAction Stop
                $valid = $thisDialogOptions.title -contains $Properties[$key]
                if (-not $valid) {
                    $atLeastOneFailed = $true
                    Write-Warning "Value '$($Properties[$key])' for property '$key' is not valid for category '$($Category)'. Valid values are: $($thisDialogOptions.title -join ', ')"
                    continue    # we leave it to the caller to break the loop by using -ErrorAction Stop
                }
                $validProperties[$key] = $Properties[$key]
            }
            'multiselect' {         # type "multiselect"; never so "multiselect_plus" in the field, just a guess
                $thisDialogOptions = Get-IdoitDialog -params @{ category = $Category; property = $key } -ErrorAction Stop
                foreach ($thisKey in $Properties[$key]) {
                    $valid = $thisDialogOptions.title -contains $thisKey
                    if (-not $valid) {
                        $atLeastOneFailed = $true
                        Write-Warning "Value '$thisKey' for property '$key' is not valid for category '$Category'. Valid values are: $($thisDialogOptions.title -join ', ')"
                        continue    # we leave it to the caller to break the loop by using -ErrorAction Stop
                    }
                    $validProperties[$key] = $Properties[$key]
                }
            }
            Default {
                # For other types, we assume the value is valid
                $validProperties[$key] = $Properties[$key]
            }
        }
    }
    if ($atLeastOneFailed) {
        Write-Error "One or more properties failed validation for category '$Category'. Please check the warnings above."
    }
    Write-Output $validProperties
}
#EndRegion '.\Private\ValidateProperties.ps1' 102
#Region '.\Public\Connect-IdoIt.ps1' -1

function Connect-IdoIt {
    <#
    .SYNOPSIS
        Connect-to Idoit API.
    .DESCRIPTION
        Connect-to Idoit API. This function is used to connect to the Idoit API and authenticate the user.
    .PARAMETER Uri
        The Uri to the idoit JSON-RPC API. should be like http[s]://your.i-doit.host/src/jsonrpc.php
    .PARAMETER Credential
        User with appropiate permissions to access the cmdb.
    .PARAMETER Username
        The username to connect to the Idoit API.
    .PARAMETER Password
        The password to connect to the Idoit API.
        The password is passed as a SecureString.
    .PARAMETER ApiKey
        This is the apikey you define in idoit unter Settings-> Interface-> JSON-RPC API to access the api
    .EXAMPLE
    PS> Connect-IdoIt -Uri 'https://test.uri' -Credential $credential -ApiKey 'TestApiKey'
    This will connect to the Idoit API using the provided Uri and Credential. The result of the login will be returned.
    .EXAMPLE
        PS> Connect-IdoIt -Uri 'https://test.uri' -Username 'TestUser' -Password (ConvertTo-SecureString 'TestPassword' -AsPlainText -Force) -ApiKey 'TestApiKey'
        This will connect to the Idoit API using the provided Uri, Username, Password and ApiKey. The result of the login will be returned.
    .NOTES
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [Cmdletbinding()]
    Param(
        [Parameter(Mandatory = $True, ParameterSetName = "PSCredential")]
        [Parameter(Mandatory = $True, ParameterSetName = "UserPasswordApiKey")]
        [String] $Uri,

        [Parameter(Mandatory = $true, ParameterSetName = "PSCredential")]
        [System.Management.Automation.PSCredential] $Credential,

        [Parameter(Mandatory = $True, ParameterSetName = "UserPasswordApiKey")]
        [String] $Username,

        [Parameter(Mandatory = $True, ParameterSetName = "UserPasswordApiKey")]
        [SecureString] $Password,

        [Parameter(Mandatory = $True, ParameterSetName = "PSCredential")]
        [Parameter(Mandatory = $True, ParameterSetName = "UserPasswordApiKey")]
        [String] $ApiKey
    )
    If ($PSBoundParameters['Debug']) {
        $DebugPreference = 'Continue'
    }

    switch ($PSCmdlet.ParameterSetName) {
        "UserPasswordApiKey" {
            $Script:IdoItParams["Connection"] = @{
                Uri = $Uri
                Username = $Username
                Password = $Password
                ApiKey = $ApiKey
            }
        }
        "PSCredential" {
            $Script:IdoItParams["Connection"] = @{
                Uri = $Uri
                Username = $Credential.UserName
                Password = $Credential.GetNetworkCredential().Password
                ApiKey = $ApiKey
            }
        }
        Default {
            Throw " Invalid parameter set $($PSCmdlet.ParameterSetName) specified."
        }
    }

    $Headers = @{
        "Content-Type" = "application/json"
        "X-RPC-Auth-Username" = $Script:IdoItParams["Connection"].Username
        "X-RPC-Auth-Password" = $Script:IdoItParams["Connection"].Password
    }

    $splatInvoke = @{
        Uri = $Script:IdoItParams["Connection"].Uri
        Headers = $Headers
        Method = "idoit.login"
        Params = @{}
    }
    $resultObj = Invoke-IdoIt @splatInvoke -ErrorAction Stop

    $result = [pscustomobject]@{
        Account     = $resultObj.name
        ClientName  = $resultObj.'client-name'
        ClientId    = $resultObj.'client-id'
    }
    $Script:IdoItParams["Connection"].SessionId = $resultObj.'session-id'

    # According to the i-doit API docs, this should return a version string.
    # As of version 33 is used in our environment, tt seems to be an integer. So we need to convert it to a string.
    $versionString = (Get-IdoItVersion).Version
    Try {       # ugly way to check whether the [string] is an integer
        if ("$versionString" -match '^\d+$') {
            $versionString = [Version]::new($versionString, 0)
        }
        $null = [Version]$versionString
    }

    Catch {
        Throw "IdoIt version is not a valid version string: $versionString"
    }
    $result
}
#EndRegion '.\Public\Connect-IdoIt.ps1' 108
#Region '.\Public\Disconnect-IdoIt.ps1' -1

function Disconnect-IdoIt {
    <#
        .SYNOPSIS
            Disconnect-IdoIt logs out of the IdoIt API-Session.
 
        .DESCRIPTION
            Disconnect-IdoIt logs out of the IdoIt API-Session.
 
        .EXAMPLE
            PS> Disconnect-IdoIt
            This will disconnect from idoit
 
        .NOTES
    #>

    Try {
        Invoke-IdoIt -Method "idoit.logout" -Params @{}
        $Script:IdoItParams["Connection"].SessionId = $null
    }
    Catch {
        Throw
    }
}
#EndRegion '.\Public\Disconnect-IdoIt.ps1' 23
#Region '.\Public\Get-IdoItCategory.ps1' -1

function Get-IdoItCategory {
    <#
        .SYNOPSIS
        Get category properties and values for a given object id and category.
 
        .DESCRIPTION
        Get-IdoItCategory retrieves all category properties and values for a given object id and category.
        Custom properties can be converted to a more user-friendly format (except if RawCustomCategory is set).
 
        .PARAMETER Id
        The object id of the object for which you want to retrieve category properties and values.
        Alias: ObjId
 
        .PARAMETER Category
        The category constant name for which you want to retrieve properties and values.
        Alias: Const
        This parameter is dynamic and will be populated based on the object type of the specified Id.
        If the Id is not specified, all available categories will be returned.
        If an Id is used, where no object type is defined, the parameter will not be populated.
 
        .PARAMETER Status
        The status of the category. Default is 2 (active).
 
        .PARAMETER UseCustomTitle
        If this switch is set, the custom category will be converted to a more user-friendly format.
 
        .EXAMPLE
        PS> Get-IdoItCategory -Id 12345 -Category 'C__CATG__CPU'
        Retrieves a list of items of the category 'C__CATG__CPU' and its values for the object with id 12345.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('ObjId')]
        [int] $Id,

        # dynamic parameter
        # [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        # [Alias('Const')]
        # [string] $Category,

        [Parameter(Position = 2, ParameterSetName = "Id")]
        [int] $Status = 2,

        [Switch] $UseCustomTitle
    )
    DynamicParam {
        #region Category: if user has entered an Id, try to get defined categories for this object
        if ($Id -gt 0) {
            $obj = Get-IdoItObject -Id $Id -ErrorAction SilentlyContinue
            if ($null -ne $obj) {
                $objCategoryList = Get-IdoitObjectTypeCategory -Type $obj.objecttype -ErrorAction SilentlyContinue
            }
            if ($null -ne $objCategoryList) {
                $validCatConstList = $objCategoryList | Select-Object -ExpandProperty const
            }
        } else {
            if ($null -eq $validCatConstList) {             # default: deliver all constants
                $validCatConstList = (Get-IdoItConstant | Where-Object Type -in ('GlobalCategory','SpecificCategory')).Name
            }
        }
        $dynParamCategory = NewDynamicParameter -Name 'Category' -ParameterType 'System.String' -ValidateSet $validCatConstList -Mandatory $true

        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $RuntimeParameterDictionary.Add('Category', $dynParamCategory)
        #endregion
        return $RuntimeParameterDictionary
    }
    begin {

    }

    process {
        $params = @{
            objID = $Id
            category = $PSBoundParameters['Category']
            status = $Status
        }
        Try {
            $result = Invoke-Idoit -Method 'cmdb.category.read' -Params $params
        } Catch {
            $ex = $_
            if ($ex.Exception.Message -match 'Virtual category') {
                # Write-Warning "Skipping virtual category: $($params.category)"
                return
            }
            Throw $_
        }
        # This one more wierd things of the i-doit API:
        # If the category is not defined for the object, it returns an empty array with only the objId and id properties.
        # But sometimes it returns an empty object without any properties.
        # If the category is defined, it returns an array with the properties of the category.

        # We will normalize the two "empty" result in returning $null
        # I really hate this solution, but didnt find a better way to handle this.
        if ( @($result).Count -eq 0 ) {
            $result = $null
        }
        if ( @($result.PSObject.Properties).count -le 2 ) {
            # if the result has max two properties, it is an empty category
            $result = $null
        }
        if ($null -ne $result) {
            foreach ($item in $result) {
                if ($UseCustomTitle) {
                    # is it a custom category?
                    # TODO: It is not very efficient to do all the stuff for each object. Check that later.
                    $objTypeCatList = Get-IdoitObjectTypeCategory -ObjId $Id -ErrorAction Stop
                    $objTypeCat = $objTypeCatList | Where-Object { $_.const -eq $params.category }
                    if ($null -eq $objTypeCat) {
                        Write-Error "ObjectTypeCategory '$($params.category)' not found for object with ID $Id."
                        return
                    }
                    $isCustom = $objTypeCat.type -eq 'custom'
                    if ($isCustom) {
                        $item = ConvertFrom-CustomCategory -InputObject $item -CategoryObject $objTypeCat
                    }
                }
                $item.PSObject.TypeNames.Insert(0, 'Idoit.Category')
                $item | Add-Member -MemberType NoteProperty -Name 'Category' -Value $params.category -Force
                $item
            }
        }
    }

    end {

    }
}
#EndRegion '.\Public\Get-IdoItCategory.ps1' 130
#Region '.\Public\Get-IdoitCategoryInfo.ps1' -1

Function Get-IdoitCategoryInfo {
    <#
        .SYNOPSIS
        Get-IdoitCategoryInfo
 
        .DESCRIPTION
        Get-IdoItCategoryInfo lets you discover all available category properties for a given category id.
        The list corresponds to the properties you will receive for each returned object after calling the cmdb.category.read method.
 
        .PARAMETER Category
        Look for category info by category name. This is the most common way to get category info.
 
        .PARAMETER CatgId
        Look for category info by category id of a global category.
 
        .PARAMETER CatsId
        Look for category info by category id of a s(?) category.
 
        .EXAMPLE
        PS>Get-IdoitCategoryInfo -Category 'C__CATG__CPU'
        Gives you detailed Info about every possible categaory value of this object.
        E.g. for 'C__CATG__CPU' you get title, manifacturer, type, frequency, cores, etc.
        ... cores: @{title=CPU cores; check=; info=; data=; ui=}
                   @{
                        title = 'CPU cores';
                        check = @{ mandatory = 'False' };
                        info = @{ primary_field = 'False'; type = 'int'; backward = 'False'; title = 'LC__CMDB__CATG__CPU_CORES'; description = 'CPU cores' };
                        data = @{ type = 'int'; readonly = 'False'; index = 'False'; field = 'isys_catg_cpu_list__cores' };
                        ui = @{ type = 'text'; params = @{ p_strPlaceholder = 0; default = 0; p_strClass = 'input-mini' }; default = 1; id = 'C__CATG__CPU_CORES' }
                    };
 
        .NOTES
        One more example of the incosistency of the i-doit API:
        Searching by category name does return the name as well. Searching for the same category by id does not return the name.
    #>

        Param (
            [Parameter(Mandatory = $True, ParameterSetName="Category")]
            [String]$Category,

            [Parameter(Mandatory = $True, ParameterSetName="CatgId")]
            [int]$CatgId,

            [Parameter(Mandatory = $True, ParameterSetName="CatsId")]
            [int]$CatsId
        )

        $params = @{}
        Switch ($PSCmdlet.ParameterSetName) {
            "Category" { $params.Add("category", $Category); break }
            "CatgId" { $params.Add("catgID",$CatgId); break }
            "CatsId" { $params.Add("catsID",$CatsId); break }
        }

        $result = Invoke-IdoIt -Method "cmdb.category_info.read" -Params $params -ErrorAction Stop
        $result.PSObject.Typenames.Add('Idoit.CategoryInfo')
        $result | Add-Member -MemberType NoteProperty -Name Category -Value $Category -Force
        return $result
    }
#EndRegion '.\Public\Get-IdoitCategoryInfo.ps1' 59
#Region '.\Public\Get-IdoItConstant.ps1' -1

Function Get-IdoItConstant {
    <#
        .SYNOPSIS
        Get-IdoItConstant
 
        .DESCRIPTION
        It retrieve all constants from the API (objTypes, categories, global and specific categories).
 
        .OUTPUTS
        Returns a list Type of constant, name and value.
 
        .EXAMPLE
        Get-IdoItConstant
        Returns all the constants.
 
        .NOTES
    #>

        [CmdletBinding()]
        $params = @{}

        $result = Invoke-IdoIt -Method "idoit.constants" -Params $params
        # one of the things, which are wierd in this API
        # result is an object having a property by type of constant
        # e.g. $result.objectTypes, $result.Categories.G, $result.Categories.S, recordStates, ...
        # Each of this properties is set up as a object having a property by contstant name
        # and a value of the text (or title?) of that constant

        Convert-PropertyToArray -InputObject $result.objectTypes | Select-Object @{ name='Type'; Expression={'ObjectType'} }, Name, Value
        Convert-PropertyToArray -InputObject $result.recordStates | Select-Object @{ name='Type'; Expression={'RecordState'} }, Name, Value
        Convert-PropertyToArray -InputObject $result.Categories.G | Select-Object @{ name='Type'; Expression={'GlobalCategory'} }, Name, Value
        Convert-PropertyToArray -InputObject $result.Categories.S | Select-Object @{ name='Type'; Expression={'SpecificCategory'} }, Name, Value
        Convert-PropertyToArray -InputObject $result.Categories.g_custom | Select-Object @{ name='Type'; Expression={'CustomCategory'} }, Name, Value
        Convert-PropertyToArray -InputObject $result.relationTypes | Select-Object @{ name='Type'; Expression={'RelationType'} }, Name, Value
        Convert-PropertyToArray -InputObject $result.staticObjects | Select-Object @{ name='Type'; Expression={'StaticObject'} }, Name, Value
    }
#EndRegion '.\Public\Get-IdoItConstant.ps1' 36
#Region '.\Public\Get-IdoitDialog.ps1' -1

function Get-IdoitDialog {
    <#
    .SYNOPSIS
        Get the dialog for a specific category and property from i-doit.
 
    .DESCRIPTION
        This function retrieves the dialog for a specific category and property.
        It returns a list of options available for the specified category and property.
 
    .PARAMETER params
        A hashtable containing the parameters for the dialog.
        The hashtable should contain at least the keys 'category' and 'property'.
        Example: @{ category='C__CATG__CPU'; property='manufacturer' }
 
    .EXAMPLE
        Get-IdoitDialog -params @{ category='C__CATG__CPU'; property='manufacturer' }
        Retrieves the dialog options for the CPU category and manufacturer property.
 
    .NOTES
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable] $params             # parameters for the dialog, e.g. @{ category='C__CATG__CPU'; property='manufacturer' }
    )

    begin {

    }

    process {
        $apiResult = Invoke-IdoIt -Endpoint 'cmdb.dialog' -Params $params
        foreach ($location in $apiResult) {
            $location | Add-Member -MemberType NoteProperty -Name 'ParentId' -Value $Id -Force -PassThru
        }
    }

    end {

    }
}
#EndRegion '.\Public\Get-IdoitDialog.ps1' 43
#Region '.\Public\Get-IdoitLocationTree.ps1' -1

function Get-IdoitLocationTree {
    <#
    .SYNOPSIS
        Get the next location tree from i-doit
 
    .DESCRIPTION
        This function retrieves the location tree from i-doit.
        The root location is returned if no Id is specified.
 
    .PARAMETER Id
        The Id of the location to retrieve the tree from.
        If 0 is specified, the root location is returned.
 
    .EXAMPLE
        Get-IdoitLocationTree -Id 0
        Retrieves the root location.
 
    .EXAMPLE
        Get-IdoitLocationTree -Id 1
        Retrieves the location tree starting from the location with Id 1 (root).
 
 
    >#>

    [CmdletBinding()]
    param (
        [int] $Id       # 0 returns the root location
    )

    begin {

    }

    process {
        $apiResult = Invoke-IdoIt -Endpoint 'cmdb.location_tree' -Params @{ id = $Id }
        foreach ($location in $apiResult) {
            $location.PSObject.TypeNames.Insert(0, 'PSIdoitNG.Location')
            $location | Add-Member -MemberType NoteProperty -Name 'ParentId' -Value $Id -Force -PassThru
        }
    }

    end {

    }
}
#EndRegion '.\Public\Get-IdoitLocationTree.ps1' 45
#Region '.\Public\Get-IdoitObject.ps1' -1

function Get-IdoitObject {
    <#
      .SYNOPSIS
      Get-IdoitObject returns an object from the i-doit API or $null.
 
      .DESCRIPTION
      Get-IdoitObject returns an object or $null.
 
      .EXAMPLE
      Get-IdoitObject -Id 540
 
      .PARAMETER Id
       The id of the object you want to retrieve from the i-doit API.
      #>

    [cmdletBinding(ConfirmImpact = 'Low')]
    [OutputType([Object])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [int] $Id
    )

    process {
        $apiResult = Invoke-Idoit -Method 'cmdb.object.read' -Params @{ id = $Id }
        if ($null -ne $apiResult) {
            $apiResult.PSObject.TypeNames.Insert(0, 'Idoit.Object')
        }
        else {
            Write-Error -Message "Object not found Id=$Id"
        }
        $apiResult
    }
}

#EndRegion '.\Public\Get-IdoitObject.ps1' 35
#Region '.\Public\Get-IdoitObjectTree.ps1' -1

function Get-IdoitObjectTree {
    <#
    .SYNOPSIS
    Retrieves the full object tree for a given i-doit object ID.
 
    .DESCRIPTION
    This cmdlet retrieves the object tree for a specified i-doit object ID, including its categories and properties.
    It excludes categories specified in the `ExcludeCategory` parameter.
 
    If you have installed the module "PwshSpectreConsole" you can get a nice view of the results by using:
    Format-SpectreJson -Data (Get-IdoitObjectTree -id 37) -Depth 5
 
    .PARAMETER Id
    The ID of the i-doit object for which to retrieve the tree.
 
    .PARAMETER ExcludeCategory
    An array of category constants to exclude from the results. Default is 'C__CATG__LOGBOOK'.
 
    .PARAMETER IncludeEmptyCategories
    A switch to include empty categories in the output.
 
    .EXAMPLE
    Get-IdoitObjectTree -Id 37
    Id Title ObjectType Categories
    -- ----- ---------- ----------
    37 This Title 53 {@{Category=C__CATG__OVERVIEW; Properties=}, @{Category=C__CATG__RELATION; Properties=System.Object[]}, @{Category=C__CATG__M...
 
    .EXAMPLE
    PSIdoitNG> Get-IdoitObjectTree -Id 37 | Select-Object -Expanded Categories
    -------- ----------
    C__CATG__OVERVIEW @{id=4; objID=37}
    C__CATG__RELATION {@{id=15; objID=53; object1=; object2=; relation_type=; weighting=; itservice=; description=}, @{id=63; objID=156; objec...
    C__CATG__MAIL_ADDRESSES @{id=353; objID=37; title=thisTitle@somewhere; primary_mail=thisTitle@somewhere; primary=; descrip...
    C__CATG__GLOBAL @{id=37; objID=37; title=thisTitle; status=; created=2024-10-09 12:56:39; created_by=SomeBody; changed=2025-05-...
    C__CATS__PERSON @{id=11; objID=37; title=thisTitle@somewhere; salutation=; first_name=First; last_name=Last; academic_de...
 
    PSIdoitNG> $x.Categories[3].Properties
    id : 37
    objID : 37
    title : This Title
    status : @{id=2; title=Normal; const=; title_lang=LC__CMDB__RECORD_STATUS__NORMAL}
    created : 2024-10-09 12:56:39
    created_by : SomeBody
    changed : 2025-05-27 23:10:18
    changed_by : automation
    purpose :
    category :
    sysid : SYSID_1733071461
    cmdb_status : @{id=6; title=in operation; const=C__CMDB_STATUS__IN_OPERATION; title_lang=LC__CMDB_STATUS__IN_OPERATION}
    type : @{id=53; title=Persons; const=C__OBJTYPE__PERSON; title_lang=LC__CONTACT__TREE__PERSON}
    tag :
    description :
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [int] $Id,

        [string[]] $ExcludeCategory = 'C__CATG__LOGBOOK',

        [switch] $IncludeEmptyCategories
    )

    begin {

    }

    process {
        $obj = Get-IdoitObject -Id $Id
        if ($null -eq $obj) {
            Write-Error "Object with ID $Id not found."
            return
        }
        $catList = Get-IdoItObjectTypeCategory -Type $obj.objecttype
        if ($null -eq $catList) {
            Write-Error "No categories found for object with ID $Id."
            return
        }
        $result = [PSCustomObject]@{
            Id = $Id
            Title = $obj.title
            ObjectType = $obj.objecttype
            Categories = @()
        }
        foreach ($category in $catList) {
            if ($category.const -in $ExcludeCategory) {
                continue
            }
            $catValues = Get-IdoItCategory -Id $Id -Category $category.const -ErrorAction Stop
            # an logically empty result contains objId, id plus the category const added by Get-IdoitCategory
            if ( $null -eq $catValues -and -not $IncludeEmptyCategories ) {
                continue
            }
            # $catValues | ft -GroupBy RefCategory -AutoSize -Wrap
            $result.Categories += [PSCustomObject]@{
                Category = $category.const
                Properties = $catValues
            }
        }
        $result
    }

    end {

    }
}
#EndRegion '.\Public\Get-IdoitObjectTree.ps1' 109
#Region '.\Public\Get-IdoItObjectType.ps1' -1

function Get-IdoItObjectType {
    <#
    .SYNOPSIS
        Get-IdoItObjectType retrieves object types from i-doit.
 
    .DESCRIPTION
        Get-IdoItObjectType retrieves object types from i-doit. You can specify the object type by its ID or title.
 
    .PARAMETER Id
        The ID of the object type to retrieve. This parameter is mandatory.
 
    .PARAMETER Const
        The title of the object type to retrieve. This parameter is mandatory.
 
    .PARAMETER Enabled
        If specified, only enabled object types will be retrieved.
 
    .PARAMETER Skip
        The number of object types to skip before returning results. This is useful for pagination.
        The default value is 0.
 
    .PARAMETER Limit
        The maximum number of object types to return. This is useful for pagination.
        The default value is 100.
 
    .EXAMPLE
        Get-IdoItObjectType -Id 1
        Retrieves the object type with ID 1.^
 
    .EXAMPLE
        Get-IdoItObjectType -Const "C_OBJTYPE_SERVICE","C_OBJTYPE_SERVER"
        Retrieves the object types with titles "C_OBJTYPE_SERVICE" and "C_OBJTYPE_SERVER".
 
    .EXAMPLE
        Get-IdoItObjectType -Enabled
        Retrieves all enabled object types.
 
    .EXAMPLE
        Get-IdoItObjectType -Limit 20
        Get-IdoItObjectType -Skip 20 -Limit 20
        Retrieves the the first 20 object types and then the next 20 object types.
 
    .DESCRIPTION
        Get-IdoItObjectType retrieves object types from i-doit. You can specify the object type by its ID or title.
    #>

    [CmdletBinding()]
    Param (
        [Int[]] $Id,

        [Alias('Title')]
        [string[]] $Const,

        [Switch] $Enabled,

        [int] $Skip,
        [Int] $Limit
    )

    #checkCmdbConnection

    Process {
        $params= @{}
        $filter = @{}

        foreach ($PSBoundParameter in $PSBoundParameters.Keys) {
            switch ($PSBoundParameter) {
                "Id" {
                        $filter.Add("ids", @($Id))
                    break
                }
                "Const" {
                    $filter.Add("titles", @($Const))        # sadly: the API param is title, searched are const strings
                    break
                }
                "Enabled" {
                    $filter.Add("enabled", 1)
                    break
                }
                "Limit" {
                    if ($Skip -gt 0) {
                        $params.Add("limit", "$Skip,$Limit")
                    } else {
                        $params.Add("limit", $Limit)
                    }
                    break
                }
            }
        }
        $params.Add("filter", $filter)

        $result = Invoke-IdoIt -Method "cmdb.object_types.read" -Params $params
        $result | ForEach-Object { $_.PSObject.TypeNames.Add('Idoit.ObjectType') }
        Return $result
    }
}
#EndRegion '.\Public\Get-IdoItObjectType.ps1' 96
#Region '.\Public\Get-IdoItObjectTypeCategory.ps1' -1

Function Get-IdoItObjectTypeCategory {
    <#
    .SYNOPSIS
    Get-IdoItObjectTypeCategory
 
    .DESCRIPTION
    Gets all the categories that the specified object type is constructed of.
 
    .PARAMETER ObjId
    Object ID for which the categories should be returned. If this parameter is specified, the type of the object will be determined first.
 
    .PARAMETER Type
    Object type for which the categories should be returned. This can be a string or an integer.
 
    .OUTPUTS
    Returns a collection of IdoIt.ObjectTypeCategory objects. Each object represents a category.
 
    .EXAMPLE
    PS> Get-IdoItObjectTypeCategory -Type 'C__OBJTYPE__SERVER'
 
    This will get all categories that are assigned to the ObjectType 'Server'
    [PSCustomObject] @{ id = 31; title = 'Overview page'; const = 'C__CATG__OVERVIEW'; multi_value = 0; source_table = 'isys_catg_overview' }
    [PSCustomObject] @{ id = 42; title = 'Drive'; const = 'C__CATG__DRIVE'; multi_value = 1; source_table = 'isys_catg_drive' }
    ...
 
    .EXAMPLE
    PS> Get-IdoItObjectTypeCategory -Type 1
    This will get all categories that are assigned to the ObjectType with ID 1.
 
    .EXAMPLE
    PS> Get-IdoItObjectTypeCategory -ObjId 540
    This will get all categories for that object id (Server with ID 540).
 
    .NOTES
    #>

    [CmdletBinding(DefaultParameterSetName = 'ObjectType')]
    [OutputType([System.Object[]])]
    Param (
        [Parameter (Mandatory = $false, ValueFromPipelineByPropertyName = $True, ParameterSetName = 'ObjectId')]
        [ValidateNotNullOrEmpty()]
        [int] $ObjId,

        [Parameter (Mandatory = $True, ValueFromPipeline = $True, ParameterSetName = 'ObjectType')]
        [ValidateNotNullOrEmpty()]
        [Alias('TypeId','Id')]
        $Type
    )

    Process {
        $params = @{}
        switch ($PSCmdlet.ParameterSetName) {
            'ObjectId' {
                # if we have an object id, we need to get the type first
                $obj = Get-IdoItObject -Id $ObjId
                if ($null -eq $obj) {
                    Write-Error "Object with ID $ObjId not found."
                    return
                }
                $Type = $obj.objecttype
            }
            'ObjectType' {
                # do nothing, type is already set
            }
        }
        $params.Add("type", $Type)

        $result = Invoke-IdoIt -Method "cmdb.object_type_categories.read" -Params $params

        #idoit delivers two arrays, depending of global or specific categories. From a PowerShell
        #point of view this is ugly - so we flatten the result into one PSObject.

        ForEach ($thisProperty In $result.PSObject.Properties) {
            ForEach ($subProperty In $result.($thisProperty.Name)) {
                if ([string]::IsNullOrEmpty($subProperty.type)) {
                    # older API version seems not to deliver the type?
                    $subProperty | Add-Member -MemberType NoteProperty -Name "type" -Value $thisProperty.Name
                }
                $subProperty.PsObject.TypeNames.Insert(0,'IdoIt.ObjectTypeCategory')
                $subProperty
            }
        }
    }
}
#EndRegion '.\Public\Get-IdoItObjectTypeCategory.ps1' 84
#Region '.\Public\Get-IdoItObjectTypeGroup.ps1' -1

Function Get-IdoItObjectTypeGroup {
    <#
    .SYNOPSIS
    Get-IdoItObjectTypeGroup
 
    .DESCRIPTION
    Gets all the object type groups that are available in the i-doit CMDB.
 
    .EXAMPLE
    Get-IdoItObjectTypeGroup
    Return all object type groups.
 
    .NOTES
    #>

    [CmdletBinding()]
    Param ()

    $result = Invoke-IdoIt -Method "cmdb.object_type_groups.read"
    $result | ForEach-Object {
        $_.PSObject.TypeNames.Insert(0, 'IdoIt.ObjectTypeGroup')
    }
    Write-Output $result
}
#EndRegion '.\Public\Get-IdoItObjectTypeGroup.ps1' 24
#Region '.\Public\Get-IdoItVersion.ps1' -1

Function Get-IdoItVersion {
    <#
        .SYNOPSIS
            Get the version of the Idoit instance
        .DESCRIPTION
            This function retrieves the version of the Idoit instance. Since some version of the API, it does return a integer value and not a version string.
        .EXAMPLE
            Get-IdoItVersion
            This will retrieve the version of the Idoit instance.
        .NOTES
    #>

    $result = Invoke-IdoIt -Method "idoit.version" -Params @{}
    return $result | Select-Object version, type, step
}
#EndRegion '.\Public\Get-IdoItVersion.ps1' 15
#Region '.\Public\Invoke-IdoIt.ps1' -1

Function Invoke-IdoIt {
    <#
    .SYNOPSIS
    Invoke-IdoIt API request to the i-doit RPC Endpoint
 
    .DESCRIPTION
    This function is calling the IdoIt API.
    The result is returned as a PSObject.
 
    .PARAMETER Endpoint
    This parameter the method yout want to call at the RPC Endpoint (see https://kb.i-doit.com/de/i-doit-add-ons/api/index.html).
    From my personal point of view, at the time of writing the documentation is not very good.
 
    .PARAMETER Params
    Hashtable creating request body with the methods parameters (https://kb.i-doit.com/de/i-doit-add-ons/api/index.html).
    The following additional parameters are inserted into the request body: ApiKey, Request id, Version
 
    .PARAMETER Headers
    Optional parameter if default headers should be overwritten (e.g. when logging into a new session).
 
    .PARAMETER Uri
        The Uri if the API Endpoint.
 
    .PARAMETER Version
        The version of the API. Default is 2.0
 
    .PARAMETER ApiKey
        The API key to be used. Default is the one used in Connect.
 
    .EXAMPLE
        $result = -Method "idoit.logout" -Params @{}
 
    .NOTES
    To trace the API calls, set the global variable $Global:IdoitApiTrace to @().
    For every API call, a new entry is added to the array.
    The entry contains the following properties:
        Endpoint: The endpoint called
        Request: The request body
        Response: The response body (if response was successful)
        Time: The time of the call
        Exception: The exception thrown (if any)
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [CmdletBinding()]
    Param (
        [Parameter( Mandatory = $True )]
        [ValidateNotNullOrEmpty()]
        [Alias('Method')]
        [String] $Endpoint,

        [ValidateNotNull()]
        [Hashtable] $Params = @{},

        [Hashtable] $Headers = @{"Content-Type" = "application/json"; "X-RPC-Auth-Session" = $Script:IdoItParams["Connection"].SessionId},

        [String] $Uri = $Script:IdoItParams["Connection"].Uri,

        [string] $Version = "2.0",

        [string] $ApiKey = $Script:IdoItParams["Connection"].ApiKey
    )

    $Params['apikey'] = $ApiKey
    $body = @{
        "method" = $Endpoint
        "version" = $Version
        "id" = [Guid]::NewGuid()
        "params" = $Params
    }
    $bodyJson = ConvertTo-Json -InputObject $body -Depth 4

    Try {
        $apiResult = Invoke-RestMethod -Uri $Uri -Method Post -Body $bodyJson -Headers $Headers
        # remove quotes from integer values
        $apiResult = ($apiResult | ConvertTo-Json -Depth 10) -replace '(?m)"([0-9]+)"','$1' | ConvertFrom-Json
        if ($null -ne $Global:IdoitApiTrace) {
            $Global:IdoitApiTrace += [PSCustomObject]@{
                Endpoint = $Endpoint
                Request = [PSCustomObject]$body
                Response = $apiResult
                Time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            }
        }
    }
    Catch {
        if ($null -ne $Global:IdoitApiTrace) {
            $Global:IdoitApiTrace += [PSCustomObject]@{
                Endpoint = $Endpoint
                Request = [PSCustomObject]$body
                Exception = $_
                Time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            }
        }
        if ($_.CategoryInfo.Reason -eq 'UriFormatException') {
            # check if the login was missing
            if ([string]::IsNullOrEmpty($Script:IdoItParams["Connection"].SessionId)) {
                Write-Error -Message "No valid session found. Please check your API connection." -ErrorAction Stop
            }
        }
        Throw $_
    }

    If ($apiResult.PSObject.Properties.Name -Contains 'Error') {
        $errMsg = "Error $($apiResult.Error.Code) - $($apiResult.error.data.Description) - $($apiResult.error.message)"
        Write-Error $errMsg
    } else {
        If ( $body.Id -ne $apiResult.id) {
            Throw "Request id mismatch. Expected value was $RequestID but it is $($apiResult.id)"
        }
        if ($apiResult.result.PSObject.Properties.Name -Contains 'Success') {           # cast success to boolean
            $apiResult.result.success = $apiResult.result.success -eq 'True'
        }
        $apiResult.result
    }
}
#EndRegion '.\Public\Invoke-IdoIt.ps1' 116
#Region '.\Public\New-IdoitObject.ps1' -1

function New-IdoitObject {
    <#
    .SYNOPSIS
    Create a new i-doit object.
 
    .DESCRIPTION
    This function creates a new object in i-doit with the specified name, type, category, purpose, status, and description.
    It checks if an object with the same name already exists, and if so, it throws an error unless the `-AllowDuplicates` switch is set.
 
    .PARAMETER Name
    The name of the object to create. This is a mandatory parameter. Alias: Title
 
    .PARAMETER ObjectType
    The type of the object to create.
 
    .PARAMETER Category
    An array of categories to assign to the object. This is optional.
 
    .PARAMETER Purpose
    The purpose of the object. Valid entries can be found in the i-doit documentation.
 
    .PARAMETER Status
    The status of the object. Valid entries can be found in the i-doit documentation.
 
    .PARAMETER Description
    A description for the object. This is optional.
 
    .PARAMETER AllowDuplicates
    A switch to allow creating an object with the same name as an existing object. If this switch is not set, the function will throw an error if an object with the same name already exists.
 
    .EXAMPLE
    New-IdoitObject -Name "New Server" -ObjectType "C__OBJTYPE__SERVER" -Category "C__CATG__GLOBAL" -Purpose "In production" -Status 'C__CMDB_STATUS__IN_OPERATION' -Description "This is a new server object."
    This command creates a new server object in i-doit with the specified name, type, category, purpose, status, and description.
 
    .NOTES
    The function is intended to create *new* object of a specific type.
    If you want to create an object with the same name as an existing object, use the `-AllowDuplicates` switch.
    If you want to update an existing object, use the `Set-IdoitObject` function instead.
 
    #>

    [CmdletBinding(SupportsShouldProcess = $True)]
    param (
        [Parameter( Mandatory = $True, ValueFromPipelineByPropertyName = $True, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [Alias( 'Title' )]
        [string] $Name,

        [Parameter( Mandatory = $True, ValueFromPipelineByPropertyName = $True)]
        [ValidateNotNullOrEmpty()]
        [string] $ObjectType,

        [Parameter( ValueFromPipelineByPropertyName = $True)]
        [ValidateNotNullOrEmpty()]
        [string[]] $Category,

        [Parameter( ValueFromPipelineByPropertyName = $True)]
        [ValidateNotNullOrEmpty()]
        [string] $Purpose,

        [Parameter( ValueFromPipelineByPropertyName = $True)]
        [ValidateNotNullOrEmpty()]
        [Alias( 'cmbd_status')]
        [string] $Status,

        [Parameter( ValueFromPipelineByPropertyName = $True)]
        [ValidateNotNullOrEmpty()]
        [string] $Description,

        [Parameter( ValueFromPipelineByPropertyName = $True)]
        [Switch] $AllowDuplicates
    )

    begin {

    }

    process {
        # by default, an object of this type and name should not exist
        if (-not $AllowDuplicates) {
            $obj = Search-IdoItObject -Conditions @{"property" = "C__CATG__GLOBAL-title"; "comparison" = "="; "value" = $Name} -ErrorAction SilentlyContinue
            if ($null -ne $obj) {
                Write-Error "An object with the name '$Name' already exists. Use -AllowDuplicates to create a new object with the same name."
                return
            }
        }
        $params = @{
            title        = $Name
            type  = $ObjectType
        }
        if ($Category.Count -gt 0) {
            $params.categories = @($Category)       # use @(..) to ensure the value is an array
        }
        if ('' -ne $Purpose) {
            $params.purpose = $Purpose
        }
        if ('' -ne $Status) {
            $params.status = $Status
        }
        if ('' -ne $Description) {
            $params.description = $Description
        }
        if ($PSCmdlet.ShouldProcess("Creating object '$Name' of type '$ObjectType'")) {
            $apiResult = Invoke-IdoIt -Method 'cmdb.object.create' -Params $params
            if ($apiResult -and 'True' -eq $apiResult.success) {
                $ret = [PSCustomObject]@{
                    ObjId = $apiResult.id
                }
                Write-Output $ret
            } else {
                Write-Error "Failed to create object. Error: $($apiResult.message)"
            }
        }
    }

    end {

    }
}
#EndRegion '.\Public\New-IdoitObject.ps1' 119
#Region '.\Public\Remove-IdoitCategory.ps1' -1

function Remove-IdoitCategory {
    <#
    .SYNOPSIS
    Remove an i-doit object category entry.
 
    .DESCRIPTION
    This function removes an entry from a category of an object. Currently the API only supports removal of entries of multi-value categories.
 
    .PARAMETER Id
    The ID of the i-doit object.
 
    .PARAMETER Category
    The category of the i-doit object from which the entry should be removed.
 
    .PARAMETER EntryId
    The ID of the entry to be removed from the category.
 
    .EXAMPLE
    Remove-IdoitCategory -Id 12345 -Category 'C__CATG__MEMORY' -EntryId 12
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('ObjId')]
        [int] $Id,

        [Parameter(Mandatory = $true)]
        [string] $Category,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int] $EntryId
    )

    begin {

    }

    process {
        if (-not $PSCmdlet.ShouldProcess("Idoit Object with ID $Id in category '$Category'", "Remove")) {
            Write-Verbose "Skipping removal of object with ID $Id in category '$Category' due to ShouldProcess."
            return
        }
        $params = @{
            objID = $Id
            category = $Category
            id = $EntryId           # I hate this API
        }
        $apiEndpoint = 'cmdb.category.delete'
        $apiResult = Invoke-IdoIt -Endpoint $apiEndpoint -Params $params
        Write-Output $apiResult
    }

    end {

    }
}
#EndRegion '.\Public\Remove-IdoitCategory.ps1' 59
#Region '.\Public\Remove-IdoitObject.ps1' -1

function Remove-IdoitObject {
    <#
    .SYNOPSIS
    Removes an IdoIT object.
 
    .DESCRIPTION
    This function removes an IdoIT object by its ID and specified method.
    The used method must correspond to the objects current state (see I-doit documentation).
 
    .PARAMETER Id
    The ID of the object to be removed.
 
    .PARAMETER Method
    The method to use for removing the object. Valid values are 'Archive', 'Delete', 'Purge', 'QuickPurge'.
    Default is 'Archive'.
 
    .EXAMPLE
    Remove-IdoitObject -Id 12345 -Method 'Archive'
    This command will archive the object with ID 12345.
 
    .EXAMPLE
    Remove-IdoitObject -Id 12345 -Method 'QuickPurge'.
    This command will quickly purge the object from the database. This one uses a different API endpoint.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('ObjId')]
        [int] $Id,

        [ValidateSet('Archive', 'Delete','Purge','QuickPurge','')]
        [string] $Method = 'Archive'
    )

    begin {

    }

    process {
        if (-not $PSCmdlet.ShouldProcess("Idoit Object with ID $Id", "Remove using method $Method")) {
            Write-Output [PSCustomObject]@{ Success = $true; Message = "Operation dummy true due to -Whatif." }
            return
        }
        $params = @{
            id = $Id
        }
        $apiEndpoint = 'cmdb.object.delete'
        switch ($Method) {
            'Archive' { $params.status = 'C__RECORD_STATUS__ARCHIVED' }
            'Delete'  { $params.status = 'C__RECORD_STATUS__DELETED' }
            'Purge'   { $params.status = 'C__RECORD_STATUS__PURGE' }
            'QuickPurge' { $apiEndpoint = 'cmdb.object.quick_purge' }
        }
        $apiResult = Invoke-IdoIt -Endpoint $apiEndpoint -Params $params
        Write-Output $apiResult
    }

    end {

    }
}
#EndRegion '.\Public\Remove-IdoitObject.ps1' 63
#Region '.\Public\Search-IdoitObject.ps1' -1

function Search-IdoItObject {
    <#
    .SYNOPSIS
    Searches for objects in the i-doit CMDB based on specified conditions.
 
    .DESCRIPTION
    This cmdlet allows you to search for objects in the i-doit CMDB by providing an array of conditions.
    The conditions are passed as an array of hashtable entries.
 
    Against the usual naming of the functions, it implements "cmdb.condition.read".
 
    .PARAMETER Conditions
    An array of hashtable entries defining the search conditions. Each hashtable should include keys like
    "property", "operator", and "value".
 
    .PARAMETER Query
    A string representing the a simple search query. It will find all objects that match the query.
    This might be used to get a quick overview of objects in the i-doit CMDB.
 
    .EXAMPLE
    PS> Search-IdoItObject -Conditions @(
         @{ "property" = "C__CATG__GLOBAL-title"; "comparison" = "like"; "value" = "*r540*" },
         @{ "property" = "C__CATG__GLOBAL-type"; "comparison" = "="; "value" = "5" }
     )
 
    This will search for objects where the title contains "Server" and the type is "Server".
 
     id title sysid type created updated type_title type_icon type_group_title status
     -- ----- ----- ---- ------- ------- ---------- --------- ---------------- ------
    540 server540 SYSID_1730365404 5 2024-10-31 09:54:24 2025-05-15 16:05:08 Server /cmdb/object-type/image/5 2
 
    .NOTES
    API version 33 behaviour (or some other?)
 
    Be aware that some files are case sensitive! I know, that this is not the best practice, but I don't know who designed this.
    not case sensitive (title): Search-IdoItObject -Conditions @{"property" = "C__CATG__GLOBAL-title"; "comparison" = "="; "value" = "yOuR-Server"}
        returns records
    but case sensitive (type) : Search-IdoItObject -Conditions @{"property" = "C__CATG__GLOBAL-type"; "comparison" = "="; "value" = "C__OBJTYPE__SERVER"}
        does not return records
 
    Receiving error message like "Failed to execute the search: Error code -32099 ..."
    This might happen, if you want to select a field, which is not part of the database table your (implicit) searching.
    E.g.
    Search-IdoItObject -Conditions @{"property" = "C__CATG__GLOBAL-type"; "comparison" = "="; "value" = "5"} | ft
    returns the field type_title
    id title sysid type created updated type_title type_icon type_group_title status
    -- ----- ----- ---- ------- ------- ---------- --------- ---------------- ------
    540 server540 SYSID_1730365404 5 2024-10-31 09:54:24 2025-04-27 06:52:30 Server /cmdb/object-type/image/5 2
 
    Sorry, it seems not possible to search for a field of this name
    Search-IdoItObject -Conditions @{"property" = "C__CATG__GLOBAL-type_title"; "comparison" = "="; "value" = "Server"} | ft
    returns an error message like this:
    Exception: C:\Users\wagnerw\Lokal\Github\psidoit\psidoit\Public\Search-IdoItObject.ps1:49:17
    Line |
    49 | Throw "Failed to execute the search: $_"
        | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        | Failed to execute the search: Error code -32099 - i-doit system error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use
        | near ')' at line 7 -
 
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Conditions')]
        [hashtable[]]$Conditions,
        [Parameter(Mandatory=$true, ParameterSetName='Query')]
        [string]$Query
    )

    process {
        try {
            switch ($PSCmdlet.ParameterSetName) {
                'Conditions' {
                    $result = Invoke-IdoIt -Endpoint 'cmdb.condition.read' -Params @{ conditions = $Conditions }
                    $result = $result | ForEach-Object {
                        $_.PSObject.TypeNames.Insert(0, 'IdoIt.ConditionalSearchResult')
                        $_
                    }
                    Write-Output $result
                }
                'Query' {
                    $result = Invoke-IdoIt -Endpoint 'idoit.search' -Params @{ q = $Query }
                    $result = $result | ForEach-Object {
                        $_.PSObject.TypeNames.Insert(0, 'IdoIt.QuerySearchResult')
                        $_
                    }
                    Write-Output $result
                }
            }
        } catch {
            Throw "Failed to execute the search: $_"
        }
    }
}
#EndRegion '.\Public\Search-IdoitObject.ps1' 95
#Region '.\Public\Set-IdoItCategory.ps1' -1

Function Set-IdoItCategory {
    <#
        .SYNOPSIS
        Set category properties and values for a given object id and category.
 
        .DESCRIPTION
        Set-IdoItCategory sets all category properties and values for a given object id and category.
 
        .PARAMETER InputObject
        An object that contains the properties to be set in the category.
        If this is used, *all* properties are set to their respective values.
 
        .PARAMETER Id
        The object id of the object for which you want to set category properties and values.
        Alias: ObjId
 
        .PARAMETER Category
        The category constant name for which you want to set properties and values.
        Alias: Const
        This is a dynamic parameter and will be set based on the objects type.
 
        .PARAMETER Data
        A hashtable containing the data to be set in the category.
        The keys of the hashtable should match the property names of the category.
 
        .EXAMPLE
        PS> Set-IdoItCategory -Id 12345 -Category 'C__CATG__CPU' -Data @{title = 'New Title'; status = 1}
    #>

    [CmdletBinding( SupportsShouldProcess = $True, DefaultParameterSetName = 'Id' )]
    Param (
        [Parameter( Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'InputObject' )]
        [PSObject] $InputObject,

        [Parameter( Mandatory = $True, ValueFromPipelineByPropertyName = $True, Position = 0, ParameterSetName = 'Id' )]
        [ValidateNotNullOrEmpty()]
        [Alias( 'ObjId' )]
        [Int] $Id,

        # dynamic parameter
        # [String] $Category,

        [Parameter( Mandatory = $True, ParameterSetName = 'Id' )]
        [Hashtable] $Data
    )
    DynamicParam {
        #region Category: if user has entered an Id, try to get defined categories for this object
        if ($Id -gt 0) {
            $obj = Get-IdoItObject -Id $Id -ErrorAction SilentlyContinue
            if ($null -eq $obj) { return }
            $objCategoryList = Get-IdoitObjectTypeCategory -Type $obj.objecttype -ErrorAction SilentlyContinue
            if ($null -eq $objCategoryList) { return }
            $validCatConstList = $objCategoryList | Select-Object -ExpandProperty const
            if ($null -eq $validCatConstList) { return }
        } else {
            $validCatConstList = (Get-IdoItConstant | Where-Object Type -in ('GlobalCategory','SpecificCategory','CustomCategory')).Name
        }
        $dynParamCategory = NewDynamicParameter -Name 'Category' -ParameterType 'System.String' -ValidateSet $validCatConstList -Mandatory $true

        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $RuntimeParameterDictionary.Add('Category', $dynParamCategory)
        #endregion
        return $RuntimeParameterDictionary
    }
    begin {
        # Initialize the parameters
        $params = @{}
    }
    process {
        $Category = $PSBoundParameters['Category']
        if ($InputObject) {
            $Id = $InputObject.objId
            $Category = $InputObject.Category
            if ($null -eq $Id) {
                Write-Error "InputObject does not have a valid Id property."
                return
            }
            # now convert each property in the InputObject to a hashtable entry
            $Data = @{}
            foreach ($property in $InputObject.PSObject.Properties) {
                if ($property.Name -in 'Id','ObjId','psid_custom','Category') { continue }  # skip those
                if ($null -ne $InputObject.psid_custom) {           # do we have to translage the property names?
                    if ($InputObject.psid_custom.containsKey($property.Name)) {
                        $Data[$InputObject.psid_custom[$property.Name]] = $property.Value   # # convert to i-doit custom field name
                    } else {
                        $Data[$property.Name] = $property.Value
                    }
                } else {
                    $Data[$property.Name] = $property.Value
                }
            }
        }
        $params = @{
            object   = $Id                # you wouldn't believe it, here object id must be passed as "object" (not objId!)
            category = $Category
            data     = $Data
        }
        # Validate the properties (now that they are tranlated to i-doit names)
        $params.data = ValidateProperties -Category $params.category -Properties $params.data -ErrorAction Stop
        if ($null -eq $params.data.keys) {
            $errResponse = [PSCustomObject]@{
                Success = $false
                Error   = "No valid properties found for category '$($params.category)'."
            }
            Write-Output $errResponse
            Write-Error $errResponse.Error
            return
        }

        # if the category has multi_value=1, then we need to get an entry id, otherwise we can set the values directly
        $cat = $objCategoryList | Where-Object { $_.const -eq $params.category }
        if ($cat.multi_value -eq 1 -and $Entry -eq 0) {
            $errResponse = [PSCustomObject]@{
                Success = $false
                Error  = "Category '$($params.category)' is a multi-value category. Currently(?) entry id is mandatory here."
            }
            Write-Output $errResponse
            Write-Error $errResponse
            return
        } elseif ($cat.multi_value -eq 0 -and $Data.Entry -gt 0) {
            $errResponse = [PSCustomObject]@{
                Success = $false
                Error  = "Category '$($params.category)' is a single-value category. Please do not specify an entry id."
            }
            Write-Output $errResponse
            Write-Error $errResponse
            return
        }

        If ($PSCmdlet.ShouldProcess("Updating category on object $Id")) {
            $result = Invoke-IdoIt -Method "cmdb.category.save" -Params $params
            return $result
        }
    }
}
#EndRegion '.\Public\Set-IdoItCategory.ps1' 135
#Region '.\Public\Show-IdoitObjectTree.ps1' -1

function Show-IdoitObjectTree {
    <#
    .SYNOPSIS
    Displays the full object tree for a given i-doit object ID or input object on the console.
 
    .DESCRIPTION
    This cmdlet retrieves and displays the object tree for a specified i-doit object ID or input object, including its categories and properties.
    It formats the output based on the specified style, which can be a table, JSON, or Spectre JSON format.
 
    If you have installed the module "PwshSpectreConsole", you can get a nice view of the results by using:
    Format-SpectreJson -Data (Show-IdoitObjectTree -Id 37) -Depth 5
 
    .PARAMETER Id
    The ID of the i-doit object for which to retrieve the tree. This parameter is used when the input is an object ID.
 
    .PARAMETER InputObject
    The object already containing the full tree.
 
    .PARAMETER Style
    The style in which to display the object tree. Options are 'FormatTable', 'Json', or 'SpectreJson'. Default is 'FormatTable'.
 
    .PARAMETER ExcludeCategory
    An array of category constants to exclude from the results. Default is 'C__CATG__LOGBOOK'.
 
    .PARAMETER IncludeEmptyCategories
    A switch to include empty categories in the output.
 
    .EXAMPLE
    Show-IdoitObjectTree -Id 37
    Displays the object tree for the i-doit object with ID 37 in a formatted table.
 
    .EXAMPLE
    $object = Get-IdoitObject -Id 37
    Show-IdoitObjectTree -InputObject $object -Style Json
    Displays the object tree for the i-doit object with ID 37 in JSON format.
 
    .EXAMPLE
    Show-IdoitObjectTree -Id 37 -Style SpectreJson
    Displays the object tree for the i-doit object with ID 37 in Spectre JSON format, if the Spectre module is available. Falls back to JSON if not.
 
    .NOTES
    #>

    [CmdletBinding(DefaultParameterSetName = 'ObjId')]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ObjId')]
        [ValidateNotNullOrEmpty()]
        [Alias('ObjID')]
        [int] $Id,

        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'InputObject')]
        [ValidateNotNullOrEmpty()]
        [PSObject] $InputObject,

        [ValidateSet('FormatTable', 'Json', 'SpectreJson')]
        [string] $Style = 'FormatTable',

        [string[]] $ExcludeCategory = 'C__CATG__LOGBOOK',

        [switch] $IncludeEmptyCategories
    )

    begin {

    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'InputObject') {
            $fullObj = $InputObject
        } else {
            $splatGetIdoitObjectTree = @{
                Id = $Id
                ExcludeCategory = $ExcludeCategory
                IncludeEmptyCategories = $IncludeEmptyCategories
            }
            $fullObj = Get-IdoitObjectTree @splatGetIdoitObjectTree
            if ($null -eq $fullObj) {
                Write-Error "Object with ID $Id not found or no categories available."
                return
            }
        }
        switch ($Style) {
            'FormatTable' {
                $fullObj | Select-Object -ExcludeProperty Categories | Format-Table -AutoSize -Wrap
                $fullObj.Categories | Foreach-Object {
                    $_.Properties | Format-Table -GroupBy Category -AutoSize -Wrap
                }
            }
            'Json' {
                $fullObj | ConvertTo-Json -Depth 5
            }
            'SpectreJson' {
                if (Get-Command -Name Format-SpectreJson -ErrorAction SilentlyContinue) {
                    Format-SpectreJson -Data $fullObj -Depth 5
                } else {
                    Write-Warning "ConvertTo-SpectreJson function not found. Please ensure the Spectre module is imported."
                    $fullObj | ConvertTo-Json -Depth 5
                }
            }
            default {
                Write-Error "Unknown style: $Style"
            }
        }
    }

    end {

    }
}
#EndRegion '.\Public\Show-IdoitObjectTree.ps1' 109
#Region '.\Public\Start-IdoitApiTrace.ps1' -1

function Start-IdoitApiTrace {
    <#
        .SYNOPSIS
            Start the Idoit API trace.
        .DESCRIPTION
            This function starts the Idoit API trace by initializing a global variable to store the trace data.
        .PARAMETER None
            No parameters are required for this function.
        .EXAMPLE
            Start-IdoitApiTrace
            # Starts the Idoit API trace and initializes the global variable.
        .NOTES
            Any data already in the $Global:IdoItAPITrace variable will be lost.
            This function is intended for use in testing scenarios to capture API calls.
    #>

    [CmdletBinding(SupportsShouldProcess = $True)]
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAVoidGlobalVars', '', Justification = 'Global variable is used outside this scope.')]
    param ()
    # Suppress PSUseDeclaredVarsMoreThanAssignments for $Global:IdoItAPITrace
    # because it is intentionally assigned but not used in this scope.
    if ($PSCmdlet.ShouldProcess('IdoitApiTrace', 'Start')) {
        Write-Verbose -Message 'Starting Idoit API trace...'
        $Global:IdoItAPITrace = @()
    }
}
#EndRegion '.\Public\Start-IdoitApiTrace.ps1' 26
#Region '.\Public\Stop-IdoitApiTrace.ps1' -1

function Stop-IdoitApiTrace {
    <#
        .SYNOPSIS
            Stop the Idoit API trace.
        .DESCRIPTION
            This function stops the Idoit API trace by removing the global variable that stores the trace data.
        .PARAMETER None
            No parameters are required for this function.
        .EXAMPLE
            Stop-IdoitApiTrace
            # Stops the Idoit API trace and removes the global variable.
        .NOTES
            This function is intended for use in testing scenarios to stop capturing API calls.
    #>

    [CmdletBinding(SupportsShouldProcess = $True)]
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidGlobalVars', '', Justification = 'Global variable is used outside this scope.')]
    param ()
    if ($PSCmdlet.ShouldProcess('IdoitApiTrace', 'Stop')) {
        Write-Verbose -Message 'Stopping Idoit API trace...'
        Remove-Variable -Name 'IdoitApiTrace' -Scope Global -ErrorAction SilentlyContinue
    }
}
#EndRegion '.\Public\Stop-IdoitApiTrace.ps1' 23