Public/Get-Entity.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Get-Entity { <# .SYNOPSIS Resolve an entity record (ent-*) from entities.json, following merge tombstones to the canonical record (t/1804, design §7). With no -Id, list every canonical entity in the store. .DESCRIPTION Reads the entity store and returns the record for an `ent-*` id. If the requested record is a merge tombstone (`merged_into` set), the walk follows the pointer chain to the canonical record and stamps `redirected_from` with the requested id — so a caller can update its own selection rather than showing a record it did not ask for. The walk mirrors the server resolveMergedInto (taxonomy-editor/src/server/routes/ entity.ts, t/1786): a cycle-detection set and a shared depth cap. A cycle or an exceeded cap is a DATA DEFECT and throws (not a silent 404); a genuinely absent id returns nothing. Called with NO -Id (the default parameter set), it emits every canonical entity in file order. Merge tombstones (`merged_into` set) are skipped — they are forwarding pointers, not live entities, and listing both a tombstone and its target would double up the canonical record. No `redirected_from` is stamped on a listing. ent-* ONLY (TL t/1804#2 Q2). Cross-kind resolution (node / situation / policy / organization / term) is the server's unified getEntity resolver, not this per-store cmdlet — pass an org-*/pol-* id and it is rejected. .PARAMETER Id The entity id to resolve (must match ^ent-\d+$). Omit to list all canonical entities. .PARAMETER Path Override entities.json path (fixtures/tests). Defaults to Get-EntitiesFilePath. .EXAMPLE Get-Entity -Id ent-001 .EXAMPLE 'ent-002' | Get-Entity # follows merged_into to the canonical record .EXAMPLE Get-Entity # every canonical entity in the store .LINK Import-Entity .LINK Get-Organization #> [CmdletBinding(DefaultParameterSetName = 'All')] [OutputType([PSCustomObject])] param( [Parameter(ParameterSetName = 'ById', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidatePattern('^ent-\d+$')] [string]$Id, [string]$Path ) begin { Set-StrictMode -Version Latest $entPath = if ($Path) { $Path } else { Get-EntitiesFilePath } $store = Get-EntitiesStore -Path $entPath # id -> raw record map for the whole store (feeds the merge walk). $byId = @{} if ($store.PSObject.Properties['entities']) { foreach ($e in @($store.entities)) { if ($e.PSObject.Properties['id']) { $byId[[string]$e.id] = $e } } } } process { if ($PSCmdlet.ParameterSetName -eq 'All') { # No id supplied — emit every canonical entity in file order. Tombstones # (merged_into set) are forwarding pointers, not live entities, so skip them. if ($store.PSObject.Properties['entities']) { foreach ($e in @($store.entities)) { if (-not $e.PSObject.Properties['id']) { continue } $mergedInto = if ($e.PSObject.Properties['merged_into'] -and $null -ne $e.merged_into) { [string]$e.merged_into } else { '' } if (-not [string]::IsNullOrWhiteSpace($mergedInto)) { continue } # Shallow copy so a downstream mutation never touches the cached store record. $e | Select-Object * } } return } if (-not $byId.ContainsKey($Id)) { Write-Verbose "No entity found for id '$Id'." return } # Resolve-EntityMergedInto throws on a cycle / exceeded depth cap (data defect). $canonical = Resolve-EntityMergedInto -StartId $Id -ById $byId if ($null -eq $canonical) { # Tombstone points at an id that is absent from the store -> not_found. Write-Verbose "Entity '$Id' resolves to a merged_into target that is not present." return } # Emit a shallow copy so the redirect stamp never mutates the cached store # record (and never leaks across pipeline iterations). $out = $canonical | Select-Object * $canonicalId = [string]$canonical.id if ($canonicalId -ne $Id) { # A tombstone was followed — surface the redirect (EntityDetailBase.redirected_from). Add-Member -InputObject $out -MemberType NoteProperty -Name 'redirected_from' -Value $Id -Force } return $out } } |