Public/Metaverse/Remove-JIMMetaverseObjectTypeAttribute.ps1

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

function Remove-JIMMetaverseObjectTypeAttribute {
    <#
    .SYNOPSIS
        Unassigns a custom Metaverse Attribute from a Metaverse Object Type.
 
    .DESCRIPTION
        Removes the binding between a custom Metaverse Attribute and a Metaverse Object Type. The
        cmdlet first fetches an unassign preview to decide how to proceed:
 
        - If any Metaverse Object of the target type holds a stored value for the attribute,
          unassignment is a hard block; the cmdlet refuses and reports the affected object count.
          Clear the values first.
        - If configuration evaluating against the type reads the attribute (a Scoping Criterion
          comparing it or reading it in an Expression, or an Expression in an Attribute Flow, Object
          Matching Rule or Example Data Template), the cmdlet refuses and names each one. Change or
          remove them first; JIM never removes them for you.
        - If Synchronisation Rules targeting the type otherwise reference the attribute (Attribute
          Flows, Object Matching Rules), those type-scoped references are cascade-removed alongside
          the binding. This is guarded server-side by a type-the-name confirmation, which
          the cmdlet satisfies automatically once you confirm (or pass -Force); the cascade is
          recorded as child Activities.
        - If only the plain binding exists, it is simply removed.
 
        If the attribute is not bound to the Object Type, the cmdlet reports it and does nothing.
        Built-in attributes cannot be unassigned.
 
    .PARAMETER AttributeId
        The ID of the attribute to unbind.
 
    .PARAMETER ObjectTypeId
        The ID of the Metaverse Object Type to unbind it from.
 
    .PARAMETER ChangeReason
        An optional reason for the change, recorded in the attribute's Configuration Change History.
 
    .PARAMETER Force
        Suppresses the confirmation prompt. The cmdlet still gives the name confirmation JIM requires before it
        removes configuration that uses the attribute on that Object Type.
 
    .OUTPUTS
        None.
 
    .EXAMPLE
        Remove-JIMMetaverseObjectTypeAttribute -AttributeId 42 -ObjectTypeId 1
 
        Unassigns attribute 42 from Object Type 1 after confirmation, cascade-removing any
        type-scoped references.
 
    .EXAMPLE
        Remove-JIMMetaverseObjectTypeAttribute -AttributeId 42 -ObjectTypeId 1 -Force
 
        Unassigns without an interactive prompt.
 
    .LINK
        Add-JIMMetaverseObjectTypeAttribute
        Get-JIMMetaverseAttribute
        Remove-JIMMetaverseAttribute
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType([void])]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias('Id')]
        [int]$AttributeId,

        [Parameter(Mandatory)]
        [int]$ObjectTypeId,

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

        [switch]$Force
    )

    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
        }

        # Fetch the unassign preview to decide how to proceed and to obtain the exact attribute name
        # for the server's type-the-name confirmation.
        try {
            $impact = Invoke-JIMApi -Endpoint "/api/v1/metaverse/attributes/$AttributeId/object-types/$ObjectTypeId/unassign-preview"
        }
        catch {
            Write-Error "Failed to evaluate unassignment of Metaverse Attribute $AttributeId from Object Type ${ObjectTypeId}: $_"
            return
        }

        if ($impact.builtIn) {
            Write-Error "Cannot unassign '$($impact.attributeName)': built-in attributes cannot be unassigned."
            return
        }

        if (-not $impact.wasBound) {
            Write-Warning "Metaverse Attribute '$($impact.attributeName)' is not bound to Object Type '$($impact.metaverseObjectTypeName)'. Nothing to do."
            return
        }

        if ($impact.blockedByValues) {
            Write-Error "Cannot unassign '$($impact.attributeName)' from '$($impact.metaverseObjectTypeName)': $($impact.objectsWithValues) Metaverse Object(s) of that type hold a stored value for it. Clear the values first, then retry."
            return
        }

        if ($impact.blockedByReaders) {
            $readers = (@($impact.readers) | ForEach-Object { $_.description }) -join '; '
            Write-Error "Cannot unassign '$($impact.attributeName)' from '$($impact.metaverseObjectTypeName)': configuration on that type reads it. Change or remove each one first, then retry: $readers."
            return
        }

        # References include the binding row itself; cascade items are those beyond the plain binding.
        $cascadeCount = if ($impact.references) { @($impact.references | Where-Object { $_.kind -ne 'Binding' }).Count } else { 0 }
        $action = if ($cascadeCount -gt 0) {
            "Unassign Metaverse Attribute and cascade-remove $cascadeCount type-scoped reference(s)"
        } else {
            "Unassign Metaverse Attribute from Object Type"
        }

        if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) {
            $ConfirmPreference = 'None'
        }

        $target = "$($impact.attributeName) -> $($impact.metaverseObjectTypeName)"
        if ($PSCmdlet.ShouldProcess($target, $action)) {
            Write-Verbose "Unassigning Metaverse Attribute '$($impact.attributeName)' from Object Type '$($impact.metaverseObjectTypeName)'; type-scoped references to cascade: $cascadeCount"

            try {
                # Always send confirmationName: the server requires it to match when type-scoped
                # references exist and ignores it otherwise.
                $query = "confirmationName=$([uri]::EscapeDataString([string]$impact.attributeName))"
                if ($ChangeReason) {
                    $query += "&changeReason=$([uri]::EscapeDataString($ChangeReason))"
                }
                $null = Invoke-JIMApi -Endpoint "/api/v1/metaverse/attributes/$AttributeId/object-types/$ObjectTypeId`?$query" -Method 'DELETE'

                Write-Verbose "Unassigned Metaverse Attribute '$($impact.attributeName)' from Object Type '$($impact.metaverseObjectTypeName)'"
            }
            catch {
                Write-Error "Failed to unassign Metaverse Attribute '$($impact.attributeName)' from Object Type '$($impact.metaverseObjectTypeName)': $_"
            }
        }
    }
}