FilterBox/Write-SRxFilterBranch.ps1
|
<# .SYNOPSIS Iteration-1 ISOLATED WORKER (skeleton) for the Filter Box. Writes a customization BRANCH (a hierarchical path of Terms under the layer's "Customizations" block) to the remote O365 Termstore, app-only, in a separate process so the TemplateExplorer UI never blocks. .DESIGN Reuses the PROVEN branch-walk logic already in SRxCore: Get-SRxCustomizationWriter -> [CustomizationWriter].Start($pack) We only wrap it in the isolated-process + result-contract + local-sync envelope (the same envelope proven by the Property Box worker Write-SRxTermBatch.ps1). Result contract (identical shape to the Property Box worker): - writes JSON to -ResultPath (parent polls this) - ALSO emits a single __JSON__{...} line to stdout. .PARAMETER PackPath Path to a temp JSON file holding the BranchPack (written by the launcher to avoid command-line length limits). Shape (see architecture doc §7.1): { Operation, Scope, Optype, Environment, Design, TermID, Branch: [ { Name, NodeName, KeyAttribute, KeyAttributeValue, Attributes:{...}, Deprecated, CustomizationTarget, ProvisioningActivity } , ... top..leaf ], BaselineModified } .PARAMETER ResultPath File the parent polls; this script writes the result JSON there on completion. .NOTES SKELETON: stages are marked TODO where the exact CustomizationWriter pack shape or the local-sync XML patch must be finalized. It is written to PARSE cleanly and to fail safe (always emits a result) so the UI reap loop works from day one. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RootPath, [Parameter(Mandatory)] [string]$PackPath, [Parameter(Mandatory)] [string]$ResultPath, [Parameter(Mandatory=$false)] [switch]$Force, [Parameter(Mandatory=$false)] [ValidateSet('JSON','XML')] [string]$Format = 'JSON' ) $ErrorActionPreference = 'Stop' # ------------------------------------------------------------------ result plumbing function Write-SRxFilterResult { param([bool]$Success, [bool]$Conflict, $Data, [string]$ErrorText) $payload = [pscustomobject]@{ Success = $Success Conflict = $Conflict Data = $Data Error = $ErrorText Pid = $PID Ended = (Get-Date).ToString('o') } $json = $payload | ConvertTo-Json -Depth 25 -Compress try { $dir = Split-Path -Parent $ResultPath if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } Set-Content -LiteralPath $ResultPath -Value $json -Encoding UTF8 } catch { } Write-Host ("__JSON__" + $json) } # ------------------------------------------------------------------ helpers function ConvertFrom-SRxJsonFile { param([string]$Path) if (-not (Test-Path $Path)) { throw "Input file not found: $Path" } $raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 if ([string]::IsNullOrWhiteSpace($raw)) { throw "Input file empty: $Path" } return ($raw | ConvertFrom-Json) } # Convert a JSON Branch descriptor array into the [System.Collections.Stack] of # term-node descriptors that CustomizationWriter.Start expects (leaf peeked first). function ConvertTo-SRxNodesStack { param($Branch) # array top..leaf $stack = [System.Collections.Stack]::new() # push top..leaf so that Peek() returns the LEAF (matches CustomizationWriter walk) foreach ($d in $Branch) { $attrs = @{} if ($d.Attributes) { foreach ($p in $d.Attributes.PSObject.Properties) { $attrs[$p.Name] = [string]$p.Value } } if ($d.NodeName) { $attrs['ts_NodeName'] = [string]$d.NodeName } if ($d.KeyAttribute) { $attrs['ts_KeyAttribute'] = [string]$d.KeyAttribute } if ($d.KeyAttributeValue) { $attrs['ts_KeyAttributeValue'] = [string]$d.KeyAttributeValue } if ($d.ProvisioningActivity){ $attrs['ts_ProvisioningActivity']= [string]$d.ProvisioningActivity } if ($d.CustomizationTarget) { $attrs['ts_CustomizationTarget'] = $true } if ($d.Deprecated) { $attrs['ts_Deprecated'] = $true } $termNode = [pscustomobject]@{ Name = [string]$d.Name Attributes = $attrs Scope = [int]$d.Scope xPath = [string]$d.XPath Operation = [string]$d.Operation optype = [int]$d.Optype } $stack.Push($termNode) } return ,$stack } try { # ============================================================ STAGE 1: bootstrap Set-Location -Path $RootPath $externalScript = Join-Path $RootPath 'loadmodule.ps1' . $externalScript if ($null -ne $LoadModule) { $LoadModule.Invoke("SRxCore") | Out-Null } Initialize-SRxEnv -LoadModule2 $LoadModule -RootPath $RootPath | Out-Null Connect-SRxSPOService if ($null -eq $global:SRxEnv) { if (Get-Command Start-SRxShell -ErrorAction SilentlyContinue) { Start-SRxShell -RootPath $RootPath -isJob | Out-Null } elseif (Get-Command Initialize-SRxEnv -ErrorAction SilentlyContinue) { Initialize-SRxEnv -RootPath $RootPath | Out-Null } else { throw "SRx environment could not be initialised from '$RootPath'." } } if ($null -eq $global:SRxEnv -or $null -eq $global:SRxEnv.Tenancy) { throw "global:SRxEnv or global:SRxEnv.Tenancy is null after bootstrap." } # Import the Filter Box module so we can call Sync-SRxLocalFilterBranch here. try { Import-Module (Join-Path $PSScriptRoot 'SRxFilterBox.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue } catch { } try { Import-Module (Join-Path $PSScriptRoot 'SRxCustomizationJson.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue } catch { } try { . (Join-Path $PSScriptRoot 'Write-SRxJsonCustomization.ps1') } catch { } # ============================================================ STAGE 2: read pack $pack = ConvertFrom-SRxJsonFile -Path $PackPath $operation = [string]$pack.Operation $scope = [int]$pack.Scope # ============================================================ STAGE 3: (opt) concurrency # TODO (Iteration 2): if (-not $Force -and $pack.BaselineModified) { compare host # Customizations-block LastModifiedDate; on drift -> Write-SRxFilterResult conflict; exit 2 } # ============================================================ STAGE 4: WRITE BRANCH # Reuse the proven CSOM branch walk from SRxCore. CustomizationWriter.Start expects a # $nodesPack with { nodesStack; ts_Environment; ts_Design; ts_TermID } and reads # scope/operation/optype from the PEEK'd term node. # decide effective format (JSON only for term-hosted layers 1..3; scope 0 -> XML) $effFormat = $Format if ([int]$pack.Scope -le 0) { $effFormat = 'XML' } if (-not (Get-Command Get-SRxCustomizationWriter -ErrorAction SilentlyContinue)) { throw "Get-SRxCustomizationWriter not available (SRxCore not loaded)." } if ($effFormat -eq 'JSON') { if (-not (Get-Command New-SRxJsonContainerTerm -ErrorAction SilentlyContinue)) { throw "New-SRxJsonContainerTerm not available (SRxCustomizationJson not loaded)." } if (-not (Get-Command Write-SRxJsonCustomization -ErrorAction SilentlyContinue)) { throw "Write-SRxJsonCustomization not available (dot-source it in STAGE 1)." } # build the container descriptor (Name = C_<hash>, Attributes = ts_* incl. compressed json) $container = New-SRxJsonContainerTerm -Pack $pack -Operation $operation # app-only connection (same pattern CustomizationWriter uses) $conn = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl $props = @{} foreach ($k in $container.Attributes.Keys) { $props[[string]$k] = [string]$container.Attributes[$k] } $jw = Write-SRxJsonCustomization -Connection $conn ` -Scope ([int]$pack.Scope) ` -Optype ([int]$pack.Optype) ` -Environment ([string]$pack.Environment) ` -Design ([string]$pack.Design) ` -TermID ([string]$pack.TermID) ` -ContainerName ([string]$container.Name) ` -Operation ([string]$operation) ` -Props $props if (-not $jw.Success) { throw "Direct JSON write failed: $($jw.Error)" } } else { $writer = Get-SRxCustomizationWriter # legacy XML-mirror branch (unchanged) - keep your existing else{} body $nodesStack = ConvertTo-SRxNodesStack -Branch $pack.Branch $writerPack = [pscustomobject]@{ nodesStack = ,$nodesStack ts_Environment = [string]$pack.Environment ts_Design = [string]$pack.Design ts_TermID = [string]$pack.TermID } $writer.Start($writerPack) } # ============================================================ STAGE 5: LOCAL SYNC # Patch the cached termSet.xml Customizations sub-tree for this branch + regen JSON. # (termSetFiltered.xml is deprecated/unused -> not touched.) $syncData = $null $syncWarning = $null try { if (Get-Command Sync-SRxLocalFilterBranch -ErrorAction SilentlyContinue) { if ($effFormat -eq 'JSON') { $sync = Sync-SRxLocalJsonCustomization -Pack $pack -Operation $operation } else { $sync = Sync-SRxLocalFilterBranch -Pack $pack } if ($sync.Success) { $syncData = $sync } else { $syncWarning = "Local sync failed: $($sync.Error)" } } else { $syncWarning = "Sync-SRxLocalFilterBranch not available; local cache not updated." } } catch { $syncWarning = "Local sync threw: $($_.Exception.Message)" } # ============================================================ STAGE 6: result $data = [pscustomobject]@{ Operation = $operation Scope = $scope FilterNodes = if ($syncData) { $syncData.FilterNodes } else { $null } # new _filterNodes slice OuterXml = if ($syncData) { $syncData.OuterXml } else { $null } # patched branch xml SyncWarning = $syncWarning } Write-SRxFilterResult -Success $true -Conflict $false -Data $data -ErrorText $null exit 0 } catch { $msg = $_.Exception.Message if ($_.Exception.InnerException) { $msg += " | inner: " + $_.Exception.InnerException.Message } Write-SRxFilterResult -Success $false -Conflict $false -Data $null -ErrorText $msg exit 1 } |