Public/Metaverse/Set-JIMMetaverseAttribute.ps1

# Copyright (c) Tetron Limited. All rights reserved.
# Licensed under the Tetron Commercial License. See LICENSE file in the project root.

function Set-JIMMetaverseAttribute {
    <#
    .SYNOPSIS
        Updates an existing custom Metaverse Attribute in JIM.
 
    .DESCRIPTION
        Updates a custom Metaverse Attribute. Changes are routed to the correct endpoint:
 
        - Name, RenderingHint and StandardMappings are updated together via the attribute endpoint.
        - Type and AttributePlurality are updated via the dedicated schema endpoint. Because the
          schema change is refused while any Metaverse Object holds a stored value for the
          attribute, supplying either -Type or -AttributePlurality sends both (the unspecified one
          is read from the attribute's current schema).
 
        Object Type bindings are not changed here; use Add-JIMMetaverseObjectTypeAttribute and
        Remove-JIMMetaverseObjectTypeAttribute. Built-in attributes cannot be modified.
 
    .PARAMETER Id
        The unique identifier of the Attribute to update.
 
    .PARAMETER InputObject
        Attribute object to update (from pipeline).
 
    .PARAMETER Name
        The new name for the Attribute. Subject to a case-insensitive uniqueness check.
 
    .PARAMETER RenderingHint
        The rendering hint for multi-valued attributes.
        Valid values: Default, Table, ChipSet, List
 
    .PARAMETER Type
        The new data type for the attribute.
        Valid values: Text, Integer, LongNumber, Decimal, DateTime, Boolean, Reference, Guid, Binary
 
    .PARAMETER AttributePlurality
        The new plurality setting.
        Valid values: SingleValued, MultiValued
 
    .PARAMETER StandardMappings
        The attribute's full set of Standard Mappings, replacing any existing ones; pass an empty
        array (@()) to clear them. Each element is a hashtable with a Standard ('Scim', 'Ldap' or
        'Jim'), a CounterpartName (the equivalent attribute name in that standard), and optional
        Notes. Standard Mappings are guidance only and never affect synchronisation.
 
    .PARAMETER ChangeReason
        Optional reason for the change, recorded on the audit Activity and shown in the object's
        configuration change history.
 
    .PARAMETER PassThru
        If specified, returns the updated Attribute object.
 
    .OUTPUTS
        If -PassThru is specified, returns the updated Attribute object.
 
    .EXAMPLE
        Set-JIMMetaverseAttribute -Id 1 -Name "UpdatedName"
 
        Renames the Attribute with ID 1.
 
    .EXAMPLE
        Set-JIMMetaverseAttribute -Id 1 -RenderingHint List -PassThru
 
        Changes the rendering hint and returns the updated object.
 
    .EXAMPLE
        Set-JIMMetaverseAttribute -Id 42 -StandardMappings @(@{ Standard = 'Scim'; CounterpartName = 'costCenter'; Notes = 'SCIM Enterprise User extension.' })
 
        Records how the custom attribute corresponds to its SCIM 2.0 counterpart.
 
    .EXAMPLE
        Get-JIMMetaverseAttribute -Name "CustomAttr" | Set-JIMMetaverseAttribute -Type Integer
 
        Changes an attribute's data type (refused if any object holds a stored value).
 
    .LINK
        Get-JIMMetaverseAttribute
        New-JIMMetaverseAttribute
        Remove-JIMMetaverseAttribute
        Add-JIMMetaverseObjectTypeAttribute
        Remove-JIMMetaverseObjectTypeAttribute
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ById')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ById', ValueFromPipelineByPropertyName)]
        [int]$Id,

        [Parameter(Mandatory, ParameterSetName = 'ByInputObject', ValueFromPipeline)]
        [PSCustomObject]$InputObject,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter()]
        [ValidateSet('Default', 'Table', 'ChipSet', 'List')]
        [string]$RenderingHint,

        [Parameter()]
        [ValidateSet('Text', 'Integer', 'LongNumber', 'Decimal', 'DateTime', 'Boolean', 'Reference', 'Guid', 'Binary')]
        [string]$Type,

        [Parameter()]
        [ValidateSet('SingleValued', 'MultiValued')]
        [string]$AttributePlurality,

        [Parameter()]
        [AllowEmptyCollection()]
        [array]$StandardMappings,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ChangeReason,

        [switch]$PassThru
    )

    process {
        # Check connection first
        if (-not $script:JIMConnection) {
            Write-Error "You are not connected to JIM. Run Connect-JIM -Url <your JIM URL> to authenticate, then try again."
            return
        }

        $attrId = if ($InputObject) { $InputObject.id } else { $Id }

        # Enum request fields are sent as their string names; the API rejects numeric ordinals
        # (JsonStringEnumConverter allowIntegerValues:false, PR #1060). Responses already return
        # enum names, so a value read back from the current schema is used as-is. -Type's
        # ValidateSet exposes 'Integer' as an alias for the AttributeDataType member 'Number';
        # that is normalised where -Type is applied below. Other values are exact member names.

        $metadataChanged = $PSBoundParameters.ContainsKey('Name') -or $PSBoundParameters.ContainsKey('RenderingHint') -or $PSBoundParameters.ContainsKey('StandardMappings')
        $schemaChanged = $PSBoundParameters.ContainsKey('Type') -or $PSBoundParameters.ContainsKey('AttributePlurality')

        if (-not $metadataChanged -and -not $schemaChanged) {
            Write-Warning "No updates specified. Provide -Name, -RenderingHint, -StandardMappings, -Type and/or -AttributePlurality."
            return
        }

        # Validate and normalise Standard Mappings up front, before anything is sent to the API. A supplied
        # list replaces the attribute's full set, so an empty array clears them.
        $mappingsBody = $null
        if ($PSBoundParameters.ContainsKey('StandardMappings')) {
            $validStandards = @('Scim', 'Ldap', 'Jim')
            $mappingsBody = @()
            foreach ($mapping in $StandardMappings) {
                if (-not $mapping.Standard -or [string]$mapping.Standard -notin $validStandards) {
                    Write-Error "Each Standard Mapping requires a Standard of 'Scim', 'Ldap' or 'Jim'."
                    return
                }
                if ([string]::IsNullOrWhiteSpace([string]$mapping.CounterpartName)) {
                    Write-Error "Each Standard Mapping requires a CounterpartName (the equivalent attribute name in the standard)."
                    return
                }
                $entry = @{
                    standard        = [string]$mapping.Standard
                    counterpartName = ([string]$mapping.CounterpartName).Trim()
                }
                if (-not [string]::IsNullOrWhiteSpace([string]$mapping.Notes)) { $entry.notes = ([string]$mapping.Notes).Trim() }
                $mappingsBody += $entry
            }
        }

        $displayName = if ($Name) { $Name } elseif ($InputObject -and $InputObject.name) { $InputObject.name } else { $attrId }

        if (-not $PSCmdlet.ShouldProcess($displayName, "Update Metaverse Attribute")) {
            return
        }

        try {
            # Schema change (type / plurality). The endpoint requires both values, so read the
            # current schema to fill whichever was not supplied.
            if ($schemaChanged) {
                $current = Invoke-JIMApi -Endpoint "/api/v1/metaverse/attributes/$attrId"

                $typeValue = if ($PSBoundParameters.ContainsKey('Type')) {
                    if ($Type -eq 'Integer') { 'Number' } else { $Type }
                } else {
                    $current.type
                }
                $pluralityValue = if ($PSBoundParameters.ContainsKey('AttributePlurality')) { $AttributePlurality } else { $current.attributePlurality }

                $schemaBody = @{
                    type               = $typeValue
                    attributePlurality = $pluralityValue
                }
                if ($ChangeReason) { $schemaBody.changeReason = $ChangeReason }

                Write-Verbose "Changing schema for Metaverse Attribute $attrId (type=$typeValue, plurality=$pluralityValue)"
                $null = Invoke-JIMApi -Endpoint "/api/v1/metaverse/attributes/$attrId/schema" -Method 'PATCH' -Body $schemaBody
            }

            # Name / rendering-hint change.
            if ($metadataChanged) {
                $body = @{}
                if ($PSBoundParameters.ContainsKey('Name')) { $body.name = $Name }
                if ($PSBoundParameters.ContainsKey('RenderingHint')) { $body.renderingHint = $RenderingHint }
                if ($null -ne $mappingsBody) { $body.standardMappings = $mappingsBody }
                if ($ChangeReason) { $body.changeReason = $ChangeReason }

                Write-Verbose "Updating name/rendering for Metaverse Attribute $attrId"
                $null = Invoke-JIMApi -Endpoint "/api/v1/metaverse/attributes/$attrId" -Method 'PATCH' -Body $body
            }

            if ($PassThru) {
                Invoke-JIMApi -Endpoint "/api/v1/metaverse/attributes/$attrId"
            }
        }
        catch {
            Write-Error "Failed to update Metaverse Attribute: $_"
        }
    }
}