Public/FieldSettings.ps1

## TM Field Settings
Function Get-TMNextSharedColumn {
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)][PSObject]$FieldSpecs
    )

    ## Get furthest Custom field number number in each

    for ($i = 1; $i -ne 100; $i++) {
        if (
            (-Not ($FieldSpecs.APPLICATION.fields | Where-Object { $_.field -eq 'custom' + $i })) `
                -and (-Not ($FieldSpecs.DEVICE.fields | Where-Object { $_.field -eq 'custom' + $i })) `
                -and (-Not ($FieldSpecs.DATABASE.fields | Where-Object { $_.field -eq 'custom' + $i })) `
                -and (-Not ($FieldSpecs.STORAGE.fields | Where-Object { $_.field -eq 'custom' + $i }))
        ) {
            return 'custom' + $i
        }
    }
}
Function Get-TMNextCustomColumn {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)][PSObject]$ClassFields
    )

    for ($i = 1; $i -ne 100; $i++) {
        if (-Not ($ClassFields.fields | Where-Object { $_.field -eq 'custom' + $i })) {
            return 'custom' + $i
        }
    }
}

Function Get-TMFieldSpecs {
    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $false)][Switch]$ResetIDs,
        [Parameter(Mandatory = $false)][Switch]$CustomOnly,
        [Parameter(Mandatory = $false)][Switch]$Label,
        [Parameter(Mandatory = $false)][Switch]$SkipCache
    )

    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    if (-Not $SkipCache) {
        $FieldSpecs = $TMSession.DataCache.FieldSpecs
    }

    if (-Not ($FieldSpecs.PSObject.Properties.Name -contains 'APPLICATION') -or $SkipCache) {

        #Honor SSL Settings
        $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

        $Instance = $TMSession.TMServer.Replace('/tdstm', '')
        $instance = $instance.Replace('https://', '')
        $instance = $instance.Replace('http://', '')

        $uri = 'https://'
        $uri += $instance
        $uri += '/tdstm/ws/customDomain/fieldSpec/ASSETS'

        try {
            $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings
        } catch {
            return $_
        }

        if ($response.StatusCode -eq 200) {
            $FieldSpecs = ($response.Content | ConvertFrom-Json)
            if (-Not $SkipCache) {
                $TMSession.DataCache.FieldSpecs = ($response.Content | ConvertFrom-Json)
            }
        } else {
            return 'Unable to collect Field Settings.'
        }
    }

    if ($CustomOnly) {
        foreach ($AssetClass in $FieldSpecs.PSObject.Properties.Value) {
            $AssetClass.fields = $AssetClass.fields | Where-Object { $_.udf -ne '0' }
        }
    }

    if ($ResetIDs) {
        ## Read each domain field to reset the IDs
        foreach ($AssetClass in $FieldSpecs.PSObject.Properties.Value) {

            ## Sort the fields by label and collect a new ordered list of the fields
            $AssetClass.fields = $AssetClass.fields | Sort-Object -Property 'label'

            ## Iterate over all of the fields
            for ($i = 0; $i -lt $AssetClass.fields.Count; $i++) {

                ## If the Field is a custom field.
                if ($AssetClass.fields[$i].field -like 'custom*') {

                    ## Change the custom field number to be set on import
                    $AssetClass.fields[$i].field = 'customN'
                }
                ## If the Field Control is a JSON, sets the JSONFieldID to Null.
                if ($AssetClass.fields[$i].control -eq 'JSON') {

                    $AssetClass.fields[$i].jsonFieldId = $null
                }
            }
        }
    }
    return $FieldSpecs
}
Function Update-TMFieldSpecs {
    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$FieldSpecs,
        [Parameter(Mandatory = $false)][Switch]$SkipCache
    )


    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    ## Clear related caches that will need to be updated
    $TMSession.DataCache.FieldSpecsLabelToFieldMap = $Null
    $TMSession.DataCache.FieldSpecsFieldToLabelMap = $Null

    ## Define the domain classes
    $DomainClasses = @('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')

    # Get the Existing Field Spec
    $ServerFields = Get-TMFieldSpecs -TMSession $TMSession -SkipCache

    # workaround for a bug not present since 6.4.2
    if ( $TMSession.TMVersion -lt [version] '6.4.2' ) {
        $ValuesToAdd = @('', 'Yes', 'No')
        foreach ($Domain in $DomainClasses) {
            $FieldsToUpdate = $ServerFields.$Domain.fields | Where-Object control -EQ 'YesNo'
            foreach ($field in $FieldsToUpdate) {
                try {
                    $field.constraint | Add-Member -MemberType NoteProperty -Name 'values' -Value $ValuesToAdd -ErrorAction Stop
                } catch {
                    foreach ($ValueToAdd in $ValuesToAdd) {
                        try {
                            $field.constraint.values += $ValueToAdd
                        } catch {
                            Write-Debug "Field $($field.field) already had value '$ValueToAdd'"
                        }
                    }
                }
            }
        }
    }

    # Create the Updated FieldSpec Object and clear the fields
    $UpdateFields = $ServerFields | ConvertTo-Json -Depth 100 | ConvertFrom-Json -Depth 100
    foreach ($DomainClass in $DomainClasses) {
        $UpdateFields.$DomainClass.fields = @()
    }

    # Collect the existing Asset Class Fields, updating them if they exist
    foreach ($DomainClass in $DomainClasses) {

        $ServerClassFields = $ServerFields.$DomainClass.fields
        $NewFields = $FieldSpecs.$DomainClass.fields

        foreach ($Field in $ServerClassFields) {

            ## Look for an existing field name
            $MatchingNewField = $NewFields | Where-Object { $_.label -eq $Field.label }
            if ($MatchingNewField) {

                ## Get the UPDATED Field
                $ReturnField = $MatchingNewField | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json

                if ($ReturnField.Count -gt 1) {
                    Throw "Duplicate Field Detected - $($Field.label)"
                }

                ## Update with the original Field ID to keep the existing column
                $ReturnField.field = $Field.field

                ## Set the Constraints iist
                if ($ReturnField.control -in @('List', 'YesNo')) {
                    ## Ensure that the incomming data has the right constraint(s) property
                    Add-Member -InputObject $ReturnField -NotePropertyName 'constraint' -NotePropertyValue ($ReturnField.constraint ?? $ReturnField.constraints) -Force

                    ## Check to see if it's in the list
                    ## For Each field that already exists, get the list of constraints
                    $Field.constraint.PSObject.Properties.Name | ForEach-Object {

                        ## If the ReturnField Constraints doesn't have the value, add it
                        if ($ReturnField.constraint.PSObject.Properties.Name -notcontains $_) {
                            $ReturnField.constraint.values += $_
                        }
                    }

                    ## Remove the Added field from the Incoming Field Spec object so it doesn't get added again
                    $NewFields.PSObject.Properties.Remove($MatchingNewField)
                }

                #### TODO: This is now no longe working against 61. This line has been moved iunto
                ## Remove the Added field from the Incoming Field Spec object so it doesn't get added again
                # $NewFields.PSObject.Properties.Remove($MatchingNewField)
                # $NewFields.PSObject.Properties.Remove($MatchingNewField)

            } else {

                ## There is no matching field, return the original Field
                $ReturnField = $Field
            }

            $UpdateFields.$DomainClass.fields += $ReturnField
        }
    }

    # Add Shared Fields
    ## This is only done on one asset class (0, which exists every time). Since shared fields go to all asset classes.
    $NewSharedFields = $FieldSpecs.$DomainClass[0].fields | Where-Object { $_.shared -eq 1 }
    foreach ($NewSharedField in $NewSharedFields) {

        ## Don't allow duplicates by label
        if (
            ($UpdateFields.APPLICATION.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
                -or ($UpdateFields.DEVICE.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
                -or ($UpdateFields.DATABASE.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
                -or ($UpdateFields.STORAGE.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
        ) {

            ## Field Already Exists and has been updated
            # Write-Host "Shared Field Name:"$NewSharedField.label"is already in use."

            Continue
        }

        ## Field doesn't exist
        $ReturnField = $NewSharedField | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json
        if ($ReturnField.field -eq 'customN') {
            $ReturnField.field = Get-TMNextSharedColumn $UpdateFields
        }
        $UpdateFields.APPLICATION.fields += $ReturnField
        $UpdateFields.DEVICE.fields += $ReturnField
        $UpdateFields.DATABASE.fields += $ReturnField
        $UpdateFields.STORAGE.fields += $ReturnField
        # Write-Host 'ADDING SHARED Field Name: ' $ReturnField.label
    }

    ## Add NON Shared fields, one for each Asset Class
    foreach ($DomainClass in $DomainClasses) {
        $NewNonSharedFields = $FieldSpecs.$DomainClass.fields | Where-Object { $_.shared -eq 0 }

        foreach ($Field in $NewNonSharedFields) {

            ## Don't allow duplicates by label
            if ($UpdateFields.$DomainClass.fields | Where-Object { $_.label -eq $Field.label }) {

                ## Field Already Exists and has been updated
                # Write-Host $DomainClass 'Field Name: ' $Field.label ' is already in use.'
                Continue
            }

            ## Field doesn't exist
            $ReturnField = $Field
            if ($ReturnField.field -eq 'customN') {
                $ReturnField.field = Get-TMNextCustomColumn $UpdateFields.$DomainClass
            }
            $UpdateFields.$DomainClass.fields += $ReturnField
            # Write-Host 'ADDING '$DomainClass 'Field Name: ' $Field.label

        }
    }

    ## Validate the number of fields being created
    foreach ($DomainClass in $DomainClasses) {

        ## Count the number of Custom Fields
        $CustomFieldCount = ($UpdateFields.$DomainClass.fields | Where-Object { $_.field -like 'custom*' }).Count

        ## Disallow an upload if there are more than 96 custom fields
        if ($CustomFieldCount -gt 96) {
            Throw "Adding these fields would result in a total of $CustomFieldCount fields in the $DomainClass class."
        }

        ## Adjust every domain field spec to the correct format
        ## Ensure that the incomming data has the right constraint(s) property
        $UpdateFields.$DomainClass.fields | ForEach-Object {
            Add-Member -InputObject $_ -NotePropertyName 'constraint' -NotePropertyValue ($_.constraint ?? $_.constraints) -Force
            if ($_.PSObject.Properties.Name.Contains('constraints')) {
                $_.PSObject.Properties.Remove('constraints')
   }
            Add-Member -InputObject $_ -NotePropertyName 'defaultValue' -NotePropertyValue ($_.defaultValue ?? $_.default) -Force
            if ($_.PSObject.Properties.Name.Contains('default')) {
                $_.PSObject.Properties.Remove('default')
   }
        }
    }

    ## Build the URL to post back to
    $uri = 'https://'
    $uri += $TMSession.TMServer
    $uri += '/tdstm/ws/customDomain/fieldSpec/ASSETS'

    # Capitalize the domain
    $DomainClasses | ForEach-Object {
        $UpdateFields.$_.domain = $_
    }

    ## Build an API post body with the Project ID
    $PostBody = [PSCustomObject]@{
        fieldSettings = $UpdateFields
        projectId     = $TMSession.UserContext.project.id
    } | ConvertTo-Json -Depth 100 -Compress

    $UpdateFieldsWebRequest = @{
        Uri                  = $uri
        WebSession           = $TMSession.TMWebSession
        Body                 = $PostBody
        SkipCertificateCheck = $TMSession.AllowInsecureSSL
        Method               = 'Post'
    }

    Set-TMHeaderContentType -ContentType 'JSON' -TMSession $TMSession

    try {
        $response = Invoke-WebRequest @UpdateFieldsWebRequest
        if ($response.StatusCode -eq 200) {
            $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100

            if ($ResponseJson.status -eq 'success') {

                if (-Not $SkipCache) {
                    $TMSession.DataCache.FieldSpecs = $UpdateFields
                }

                return
            } else {
                throw $ResponseJson.errors
            }
        } elseif ($response.StatusCode -eq 204) {
            return
        } else {
            throw $_
  }
    } catch {
        return $_
    }

}

Function Set-TMFieldSpecs {
    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$FieldSpecs,
        [Switch]$Force,
        [Switch]$SkipCache

    )

    ## This command is potentially destructive
    if (-Not $Force) {
        Write-Host 'This command is used to overwrite an existing field spec object and should be used with care.'
        Write-Host 'The counterpart command ' -NoNewline
        Write-Host 'Update-TMFieldSpecs ' -NoNewline -ForegroundColor Yellow
        Write-Host 'might be more useful as it merges changes into an existing FieldSpec.'
        Write-Host 'To use this command to overwrite a FieldSpec, use of the -Force switch is required'
        throw 'You must use the force switch with this command'
    }

    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    ## Clear related caches that will need to be updated
    $TMSession.DataCache.FieldSpecsLabelToFieldMap = $Null
    $TMSession.DataCache.FieldSpecsFieldToLabelMap = $Null

    #Honor SSL Settings
    $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

    ## Build the URL to post back to
    $uri = 'https://'
    $uri += $TMSession.TMServer
    $uri += '/tdstm/ws/customDomain/fieldSpec/ASSETS'

    ## Create the Field Settings body
    ## Build an API post body with the Project ID
    $PostBody = [PSCustomObject]@{
        fieldSettings = $FieldSpecs
        projectId     = $TMsession.UserContext.project.id
    } | ConvertTo-Json -Depth 100 -Compress

    Set-TMHeaderContentType -ContentType 'JSON' -TMSession $TMSession

    try {
        $response = Invoke-WebRequest -Method Post -Uri $uri -WebSession $TMSession.TMWebSession -Body $PostBody @TMCertSettings
        if ($response.StatusCode -eq 200) {
            $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100

            if ($ResponseJson.status -eq 'success') {
                if (-Not $SkipCache) {
                    $TMSession.DataCache.FieldSpecs = $FieldSpecs
                }
                return
            } else {
                throw $ResponseJson.errors
            }
        } elseif ($response.StatusCode -eq 204) {
            return
        } else {
            throw $_
  }
    } catch {
        return $_
    }

}

Function Add-TMFieldListValues {
    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][ValidateSet('Application', 'Database', 'Device', 'Storage')][String]$Domain,
        [Parameter(Mandatory = $true)][String]$FieldLabel,
        [Parameter(Mandatory = $true)][Array]$Values,
        [Parameter(Mandatory = $false)][ValidateSet('Asc', 'Desc')]
        [String]$SortConstraintList,
        [Switch]$SkipCache
    )

    if ($Values.Count -eq 0) {
        Write-Verbose 'There were no updates to make to the field'
        return
    }

    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    #Honor SSL Settings
    $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

    ## Define the domain classes
    $DomainClasses = @{
        Application = 'APPLICATION'
        Device      = 'DEVICE'
        Database    = 'DATABASE'
        Storage     = 'STORAGE'
    }

    # Get the Existing Field Spec
    $FieldSpecs = Get-TMFieldSpecs -TMSession $TMSession -SkipCache

    ## Create the Updated FieldSpec Object and clear the fields
    $Field = $FieldSpecs.($DomainClasses[$Domain]).fields | Where-Object { $_.label -eq $FieldLabel }
    if (-Not $Field) {
        Write-Host "Field: $($FieldLabel) not found!" -ForegroundColor Red
        return
    }

    ## Only Add values to a List control
    if ($Field -and $Field.control -ne 'List') {
        Write-Verbose "Skipping update on $($Field.label) as it is a $($Field.control) type."
        return
    }

    foreach ($NewItem in $Values) {

        ## Version 6.0.2.1+ (and TODO version 6.1?, 5x?)
        ## Requires the class names to be lower case
        $CONSTRAINTS_PROPERTY_NAME = 'constraint'

        ## Convert each Asset Class name to lowercase
        ## Check to see if it's in the list
        if (-Not $Field.$CONSTRAINTS_PROPERTY_NAME.values.Contains($NewItem)) {
            $Field.$CONSTRAINTS_PROPERTY_NAME.values += $NewItem
        }

        ## Sort the list if required
        if ($SortConstraintList -eq 'Asc') {
            $Field.$CONSTRAINTS_PROPERTY_NAME.values = $Field.$CONSTRAINTS_PROPERTY_NAME.values | Sort-Object
        }
        if ($SortConstraintList -eq 'Desc') {
            $Field.$CONSTRAINTS_PROPERTY_NAME.values = $Field.$CONSTRAINTS_PROPERTY_NAME.values | Sort-Object -Descending
        }
    }

    ## Put the data back
    $FieldSpecs.($DomainClasses[$Domain]).fields | Where-Object { $_.label -eq $FieldLabel } | ForEach-Object {
        $_ = $Field
    }

    ## Construct an update call
    $uri = "https://$($TMSession.TMServer)/tdstm/ws/customDomain/fieldSpec/ASSETS"
    Set-TMHeaderContentType -ContentType 'JSON' -TMSession $TMSession

    ## Create the Field Settings body
    ## Build an API post body with the Project ID
    $PostBody = [PSCustomObject]@{
        fieldSettings = $FieldSpecs
        projectId     = $TMsession.UserContext.project.id
    } | ConvertTo-Json -Depth 100 -Compress

    try {
        $response = Invoke-WebRequest -Method Post -Uri $uri -WebSession $TMSession.TMWebSession -Body $PostBody @TMCertSettings
        if ($response.StatusCode -eq 200) {
            $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100

            if ($ResponseJson.status -eq 'success') {
                if (-Not $SkipCache) {
                    $TMSession.DataCache.FieldSpecs = $FieldSpecs
                }
            } else {
                throw $ResponseJson.errors
            }
        } elseif ($response.StatusCode -eq 204) {
            return
        } else {
            ThrowError 'Unable to save Field Settings'
  }


    } catch {
        throw $_
    }
}
function Get-TMFieldToLabelMap {
    param(
        [Parameter(Mandatory = $false)]
        [PSObject]$TMSession = 'Default',

        [Parameter(Mandatory = $false)]
        [ValidateSet('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')]
        [String]$Domain,
        [Switch]$SkipCache
    )

    $TMSession = Get-TMSession $TMSession
    if (-Not $SkipCache) {
        $FieldLabelMap = $TMSession.DataCache.FieldSpecsFieldToLabelMap
    }

    if (-Not $FieldLabelMap.Values -or $SkipCache) {

        $AllFields = Get-TMFieldSpecs -TMSession $TMSession -SkipCache:$SkipCache
        $FieldLabelMap = @{
            TemplateKeys = @{}
        }

        foreach ($DomainName in @('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')) {
            $FieldLabelMap.Add($DomainName, @{})
            foreach ($Spec in $AllFields.$DomainName.fields) {
                # Add-Member -InputObject $FieldLabelMap.$DomainName -NotePropertyName $Spec.field -NotePropertyValue $Spec.label -Force
                Write-Verbose "Domain: $($DomainName), Field: $($Spec.field), Label: $($Spec.label)"
                $FieldLabelMap.$DomainName.Add($Spec.field, $Spec.label)

                $TemplateKey = "customN|$($DomainName)|$($Spec.label)|"
                Write-Verbose "Adding TemplateKey $TemplateKey with value $($Spec.field)"
                $FieldLabelMap.TemplateKeys.Add($TemplateKey, $Spec.field)
            }
        }
    }

    if (-Not $SkipCache) {
        $TMSession.DataCache.FieldSpecsFieldToLabelMap = $FieldLabelMap
    }

    if ($Domain) {
        $FieldLabelMap.$Domain
    } else {
        $FieldLabelMap
    }
}


function Get-TMLabelToFieldMap {
    param(
        [Parameter(Mandatory = $false)]
        [PSObject]$TMSession = 'Default',

        [Parameter(Mandatory = $false)]
        [ValidateSet('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')]
        [String]$Domain,
        [Switch]$SkipCache
    )
    $TMSession = Get-TMSession $TMSession
    if (-Not $SkipCache) {
        $LabelFieldMap = $TMSession.DataCache.FieldSpecsLabelToFieldMap
    }
    if (-Not $LabelFieldMap.Values -or $SkipCache) {
        $AllFields = Get-TMFieldSpecs -TMSession $TMSession -SkipCache:$SkipCache
        $LabelFieldMap = @{}
        foreach ($DomainName in @('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')) {
            $LabelFieldMap.Add($DomainName, @{})
            foreach ($Spec in $AllFields.$DomainName.fields) {
                $LabelFieldMap.$DomainName.Add($Spec.label, $Spec.field)
            }
        }
    }
    if (-Not $SkipCache) {
        $TMSession.DataCache.FieldSpecsLabelToFieldMap = $LabelFieldMap
    }
    if ($Domain) {
        $LabelFieldMap.$Domain
    } else {
        $LabelFieldMap
    }
}