Public/SyncRuleMappings/Set-JIMSyncRuleMapping.ps1

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

function Set-JIMSyncRuleMapping {
    <#
    .SYNOPSIS
        Changes how an Attribute Flow behaves.
 
    .DESCRIPTION
        Changes how an existing Attribute Flow behaves, leaving what it reads and writes alone.
        Only the parameters you supply are changed; everything else is left as it is.
 
        What a mapping targets, and whether its source is an attribute or an Expression, cannot be
        changed here. Those revalidate against attribute types and plurality, and for an import
        mapping they reopen its place in the attribute's Attribute Priority order, so they remain a
        Remove-JIMSyncRuleMapping followed by a New-JIMSyncRuleMapping.
 
    .PARAMETER SyncRuleId
        The ID of the Synchronisation Rule the mapping belongs to.
 
    .PARAMETER MappingId
        The ID of the mapping to change.
 
    .PARAMETER InputObject
        A mapping from Get-JIMSyncRuleMapping, through the pipeline.
 
    .PARAMETER Expression
        Replaces the mapping's expression. Expression mappings only.
 
    .PARAMETER MissingInputBehaviour
        On an expression mapping, what happens when an attribute the expression reads has no value: EvaluateAnyway
        runs it regardless and contributes what it returns; ContributeNoValue skips it and contributes nothing, so
        Attribute Priority decides; FailMapping skips it and records an error, while the object's other attributes
        still flow; and FailObject leaves the whole object untouched and errored.
 
    .PARAMETER NullIsValue
        On an import mapping, whether no value is authoritative: when the rule applies to a joined, in-scope
        Connected System Object that has no value, the Metaverse attribute is cleared rather than taken from a
        lower-priority mapping.
 
    .PARAMETER InboundValueProcessing
        Text value-processing transforms applied as the value flows to the Metaverse, as a
        comma-separated set of flag names (for example 'TreatWhitespaceAsNoValue, TrimWhitespace').
        Import mappings only.
 
    .PARAMETER CaseNormalisation
        On an import mapping, the case a text value is converted to.
 
    .PARAMETER InitialExportOnly
        On an export mapping, whether the attribute is written only when the Connected System Object is created;
        after that JIM leaves it alone, and Drift Correction does not put it back.
 
    .PARAMETER Enabled
        Enables or disables the mapping. A disabled mapping is skipped in both directions; enabling it clears the
        reason JIM recorded for disabling it, if any.
 
    .PARAMETER KeepUnique
        On an Expression mapping whose value is built from a generated value (an Email built from a generated
        Account Name), whether a target refusing the value as already in use is fixed by correcting the generated
        value. $false reports the refusal as an ordinary export error instead. Changing it releases any decision the
        generated value is waiting on. Refused on any other mapping.
 
    .PARAMETER TokenKind
        Changes a generated mapping's uniqueness token. A mapping cannot be made generated, or stop being generated,
        here: delete it and create it again.
 
    .PARAMETER SuffixStyle
        OnlyIfTaken only: whether the collision suffix is a Number or a Letter.
 
    .PARAMETER SuffixStart
        OnlyIfTaken only: the first suffix value tried once the bare base value is taken.
 
    .PARAMETER SequenceStart
        Sequence only: the lowest number this flow will ever issue. Raising it above the target
        attribute's counter moves the counter forward at save time; the response's
        Generation.SequenceSkippedAhead reports the move, and a warning names the old and new
        positions. A lower or equal value has no effect.
 
    .PARAMETER SequenceIncrement
        Sequence only: how much the counter advances per issued number.
 
    .PARAMETER FixedWidth
        Sequence only: zero-pads the number to this many digits. Supply 0 to clear the padding;
        omit to leave it unchanged.
 
    .PARAMETER OnWidthExceeded
        Sequence only: what happens when a number would no longer fit -FixedWidth. StopAndReport
        stops the object with an attributed error; AllowLonger lets the number grow past the width.
 
    .PARAMETER RandomFormat
        Random only: the shape of the token.
 
    .PARAMETER RandomLength
        Random only: the length of the token in characters. Required alongside -RandomFormat Hex
        or Digits; must not be supplied for Guid.
 
    .PARAMETER Separator
        The characters placed between the base value and the token. Supply an empty string ('' or
        $null) to clear it; omit to leave it unchanged.
 
    .PARAMETER AttemptLimit
        The maximum number of candidates tried, per object per synchronisation run, before
        generation fails hard for that object.
 
    .PARAMETER NeverReuse
        Whether a value whose assignment is deleted is retired and never issued again by this flow.
        Always treated as true for a Sequence token, whatever is supplied.
 
    .PARAMETER CollisionRemediation
        Whether JIM corrects a generated value a target rejects as already in use (Collision Remediation). Omit to
        leave it unchanged. Changing it releases any of this Attribute Flow's values held for a decision, so the next
        export tries again under the new setting.
 
    .PARAMETER ExcludeConnectedSystemId
        Generated import mappings only: replaces the IDs of the Connected Systems left out of the value's
        availability checks. Values already in use in an excluded Connected System do not stop JIM choosing
        them. Each must be a Connected System the value is exported to unchanged, otherwise the update is
        refused naming the Connected System. Pass @() to clear every exclusion, so every Connected System is
        checked again; omit to leave the exclusions unchanged. Refused for a generated value on an export
        Synchronisation Rule, which is checked only in its own Connected System.
 
    .PARAMETER SaveAnyway
        Saves even though the change stops configuration test cases passing. Without it, or
        -AcceptNewResult, such a change is refused, nothing is saved, and the error names each case.
        Configuration History records the cases saved past, by name.
 
    .PARAMETER AcceptNewResult
        Takes what the configuration now gives as what each configuration test case that stopped
        passing expects, rewriting those cases, and saves. A case whose new result cannot be
        expected (it needs review, could not run, or its Scope cannot be determined) still refuses
        the change unless -SaveAnyway is given too.
 
    .PARAMETER PassThru
        Returns the mapping as changed.
 
    .OUTPUTS
        With -PassThru, the mapping as changed; otherwise nothing. Its warnings, and any derived Attribute Flows the
        change left with a missing input, are written to the warning stream either way.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 2 -MappingId 15 -MissingInputBehaviour FailObject
 
        Stops an object with a missing input exporting at all, rather than exporting a
        Distinguished Name built around the gap.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 1 -MappingId 8 -Expression 'Lower(cs["mail"])' -PassThru
 
        Rewrites an import mapping's expression and returns the updated mapping.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 1 -MappingId 8 -Enabled $false
 
        Disables one Attribute Flow without touching the Synchronisation Rule; the mapping is
        skipped by synchronisation until it is re-enabled with -Enabled $true.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 1 -MappingId 12 -KeepUnique $false
 
        Stops correcting a generated Account Name when a target refuses the User Principal Name built
        from it (mapping 12): such a refusal is then reported as an ordinary export error.
 
    .EXAMPLE
        Get-JIMSyncRuleMapping -SyncRuleId 1 |
            Where-Object { $_.sourceType -eq 'ExpressionMapping' } |
            Set-JIMSyncRuleMapping -SyncRuleId 1 -MissingInputBehaviour FailMapping
 
        Reports every expression mapping on the Rule that meets an object with a missing input,
        rather than letting it contribute a value built around the gap.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 1 -MappingId 12 -SequenceStart 500000
 
        Raises a generated Sequence mapping's Sequence Start; if the counter was already below
        500000 this moves it forward and warns naming the old and new positions.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 1 -MappingId 12 -FixedWidth 0
 
        Clears a generated Sequence mapping's fixed width, so numbers are no longer zero-padded.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 1 -MappingId 12 -ExcludeConnectedSystemId 4, 7
 
        Leaves Connected Systems 4 and 7 out of a generated value's availability checks, replacing any
        exclusions it had.
 
    .EXAMPLE
        Set-JIMSyncRuleMapping -SyncRuleId 1 -MappingId 12 -ExcludeConnectedSystemId @()
 
        Clears a generated value's exclusions, so every Connected System it is exported to is checked again.
 
    .LINK
        Get-JIMSyncRuleMapping
        New-JIMSyncRuleMapping
        Remove-JIMSyncRuleMapping
        Get-JIMGeneratedValueSequence
        Restart-JIMGeneratedValues
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ById')]
    [OutputType('JIM.SyncRuleMappingDto')]
    param(
        [Parameter(Mandatory)]
        [int]$SyncRuleId,

        [Parameter(Mandatory, ParameterSetName = 'ById', ValueFromPipelineByPropertyName)]
        [Alias('Id')]
        [int]$MappingId,

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

        [string]$Expression,

        [ValidateSet('EvaluateAnyway', 'ContributeNoValue', 'FailMapping', 'FailObject')]
        [string]$MissingInputBehaviour,

        [bool]$NullIsValue,

        [string]$InboundValueProcessing,

        [ValidateSet('None', 'Upper', 'Lower', 'Title')]
        [string]$CaseNormalisation,

        [bool]$InitialExportOnly,

        [bool]$Enabled,

        # Keep unique (Generated Value Forms, #2023), Expression mappings only.
        [bool]$KeepUnique,

        # Unique Value Generation (#242): a generated mapping's uniqueness token settings. Refused by the
        # server for a mapping that is not currently a generated mapping.
        [ValidateSet('OnlyIfTaken', 'Sequence', 'Random')]
        [string]$TokenKind,

        [ValidateSet('Number', 'Letter')]
        [string]$SuffixStyle,

        [int]$SuffixStart,

        [long]$SequenceStart,

        [int]$SequenceIncrement,

        [int]$FixedWidth,

        [ValidateSet('StopAndReport', 'AllowLonger')]
        [string]$OnWidthExceeded,

        [ValidateSet('Guid', 'Hex', 'Digits')]
        [string]$RandomFormat,

        [int]$RandomLength,

        [string]$Separator,

        [int]$AttemptLimit,

        [bool]$NeverReuse,

        [bool]$CollisionRemediation,

        # Exclusions (release 3): replaces them; @() clears them, and is sent as an empty JSON array.
        [int[]]$ExcludeConnectedSystemId,

        [switch]$SaveAnyway,

        [switch]$AcceptNewResult,

        [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
        }

        $mapId = if ($InputObject) { $InputObject.id } else { $MappingId }

        # Only send what the caller named. An omitted setting must reach the API as absent rather than
        # as a default, or every call would silently rewrite settings it was never asked about.
        $body = @{}
        if ($PSBoundParameters.ContainsKey('Expression')) { $body.expression = $Expression }
        if ($PSBoundParameters.ContainsKey('MissingInputBehaviour')) { $body.missingInputBehaviour = $MissingInputBehaviour }
        if ($PSBoundParameters.ContainsKey('NullIsValue')) { $body.nullIsValue = $NullIsValue }
        if ($PSBoundParameters.ContainsKey('InboundValueProcessing')) { $body.inboundValueProcessing = $InboundValueProcessing }
        if ($PSBoundParameters.ContainsKey('CaseNormalisation')) { $body.caseNormalisation = $CaseNormalisation }
        if ($PSBoundParameters.ContainsKey('InitialExportOnly')) { $body.initialExportOnly = $InitialExportOnly }
        if ($PSBoundParameters.ContainsKey('Enabled')) { $body.enabled = $Enabled }
        if ($PSBoundParameters.ContainsKey('KeepUnique')) { $body.keepUnique = $KeepUnique }

        # Unique Value Generation (#242): only settings the caller named are sent, nested under `generation`
        # (matching UpdateSyncRuleMappingGenerationRequest). -FixedWidth 0 clears the padding; omitting it
        # leaves it unchanged.
        $generation = @{}
        if ($PSBoundParameters.ContainsKey('TokenKind')) { $generation.tokenKind = $TokenKind }
        if ($PSBoundParameters.ContainsKey('SuffixStyle')) { $generation.suffixStyle = $SuffixStyle }
        if ($PSBoundParameters.ContainsKey('SuffixStart')) { $generation.suffixStart = $SuffixStart }
        if ($PSBoundParameters.ContainsKey('SequenceStart')) { $generation.sequenceStart = $SequenceStart }
        if ($PSBoundParameters.ContainsKey('SequenceIncrement')) { $generation.sequenceIncrement = $SequenceIncrement }
        if ($PSBoundParameters.ContainsKey('FixedWidth')) { $generation.fixedWidth = $FixedWidth }
        if ($PSBoundParameters.ContainsKey('OnWidthExceeded')) { $generation.onWidthExceeded = $OnWidthExceeded }
        if ($PSBoundParameters.ContainsKey('RandomFormat')) { $generation.randomFormat = $RandomFormat }
        if ($PSBoundParameters.ContainsKey('RandomLength')) { $generation.randomLength = $RandomLength }
        if ($PSBoundParameters.ContainsKey('Separator')) { $generation.separator = $Separator }
        if ($PSBoundParameters.ContainsKey('AttemptLimit')) { $generation.attemptLimit = $AttemptLimit }
        if ($PSBoundParameters.ContainsKey('NeverReuse')) { $generation.neverReuse = $NeverReuse }
        if ($PSBoundParameters.ContainsKey('CollisionRemediation')) { $generation.collisionRemediation = $CollisionRemediation }
        # Always a JSON array: @() keeps a single ID from serialising as a bare number, and an empty list
        # from being dropped, since [] is what tells the API to clear the exclusions.
        if ($PSBoundParameters.ContainsKey('ExcludeConnectedSystemId')) { $generation.exclusions = @($ExcludeConnectedSystemId) }
        if ($generation.Count -gt 0) { $body.generation = $generation }

        if ($body.Count -eq 0) {
            Write-Error "No settings were supplied to change. Supply at least one of -Expression, -MissingInputBehaviour, -NullIsValue, -InboundValueProcessing, -CaseNormalisation, -InitialExportOnly, -Enabled, -KeepUnique, or a generated mapping's uniqueness token settings (-TokenKind, -SuffixStyle, -SuffixStart, -SequenceStart, -SequenceIncrement, -FixedWidth, -OnWidthExceeded, -RandomFormat, -RandomLength, -Separator, -AttemptLimit, -NeverReuse, -CollisionRemediation)."
            return
        }

        $displayName = "Mapping $mapId in Synchronisation Rule $SyncRuleId"

        if ($PSCmdlet.ShouldProcess($displayName, "Update Synchronisation Rule Mapping")) {
            Write-Verbose "Updating Synchronisation Rule Mapping: $mapId in Synchronisation Rule: $SyncRuleId"

            try {
                # The save gate (#2055 Phase 4b): what to do if the change stops configuration test cases passing.
                if ($SaveAnyway) { $body.saveAnyway = $true }
                if ($AcceptNewResult) { $body.acceptNewResults = $true }
                $result = Invoke-JIMApi -Endpoint "/api/v1/synchronisation/sync-rules/$SyncRuleId/mappings/$mapId" -Method 'PATCH' -Body $body

                Write-Verbose "Updated Synchronisation Rule Mapping: $mapId"

                # A raised -SequenceStart moves the target attribute's counter forward at save time (plan
                # decision 3); the response reports it rather than doing it silently.
                if ($result.Generation.SequenceSkippedAhead) {
                    $skip = $result.Generation.SequenceSkippedAhead
                    Write-Warning "Mapping $mapId's Sequence counter moved from $($skip.From) to $($skip.To) to honour the requested Sequence Start."
                }

                # Non-blocking warnings the save raised (for example, an Attribute Flow deriving a Metaverse
                # attribute that calls a function returning a different value each time, #1750). The save went
                # ahead; each warning is written as the server phrased it.
                foreach ($saveWarning in @($result.Warnings | Where-Object { $_ })) {
                    Write-Warning $saveWarning
                }

                # Derived flows the update left with a missing input (#1750, FR 3): reported, never blocking.
                Write-JIMDependentDerivedFlowWarning -DependentDerivedFlows $result.DependentDerivedFlows

                if ($PassThru) {
                    $result
                }
            }
            catch {
                Write-Error "Failed to update Synchronisation Rule Mapping: $_"
            }
        }
    }
}