src/cmdlets/common/SegmentHelper.ps1
# Copyright 2021, Adam Edwards # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. . (import-script ..\..\common\PreferenceHelper) add-type -TypeDefinition @' namespace AutoGraph.Model { public class GraphObject { public GraphObject(object metadata) { this.__itemMetadata = metadata; } public object __ItemMetadata() { return this.__itemMetadata; } object __itemMetadata; } } '@ ScriptClass SegmentHelper { static { const SegmentDisplayTypeName 'GraphSegmentDisplayType' const MetadataMethodName __ItemMetadata function __initialize { # NOTE: There are one or more ps1xml files that defines display formats for this type based on # on the PSTypeName. That may override behaviors like default columns defined here, though other # aspects like serialization behavior should be preserved as the ps1xml options are *merged* # with exisitng options. In particular the ps1xml file provides the ability to emit a "title row" # to the display for DOS dir-style "directory listings" like PowerShell's Get-ChildItem command. # This allows us to use this type to provide an ls-like user experience when navigating the Graph. # Primarily for this reason the ps1xml is included. These files are enabled through the # 'FormatsToProcess' field of the module manifest, but can also be dynamically updated through # Update-FormatData. __RegisterSegmentDisplayType } function IsValidLocationClass($itemClass) { $itemClass -in $this.GetValidLocationClasses() } function GetValidLocationClasses { @( '__Root' 'Singleton', 'EntitySet', 'EntityType', 'NavigationProperty' ) } function UriToSegments($parser, [Uri] $uri, $responseObject) { $graphUri = if ( $uri.IsAbsoluteUri ) { $graphRelativeUri = '' for ( $uriIndex = 2; $uriIndex -lt $uri.segments.length; $uriIndex++ ) { $graphRelativeUri += $uri.segments[$uriIndex] } $graphRelativeUri } else { $uri } $ambiguousCardinality = if ( $responseObject ) { if ( ! ( $responseObject | Get-Member -MemberType ScriptMethod __ItemContext -erroraction Ignore ) ) { throw 'The specified object is not a valid Graph response object' } # In the case of a URI returned from Graph, we make this computation because # we can't tell the difference between me/photo and me/contacts, the first # of which is a navigation to a single entity, the latter to a collection, and when you construct # a URI for the first, no additional id is needed, but one is needed for the second. Above, we assumed # the second case. Subsequent parsing will let us know if we need to re-interpret the URI. For now, # detect a hint that this re-interpretation may be necessary. $itemContext = $responseObject.__ItemContext() $itemContext.IsEntity -and $itemContext.IsCollectionMember } # The last parameter ensures that in the ambiguous case where we can't distinguish between # navigations to a collection (me/contacts) or a single entity (me/photo), we just ignore # the last segment if that cannot be parsed. See comments above on ambiguousCardinality. $parser |=> SegmentsFromUri $graphUri $ambiguousCardinality } function IsGraphSegmentType($object) { $object -is [PSCustomObject] -and $object.pstypenames.contains($SegmentDisplayTypeName) } function ToPublicSegment($parser, $segment, $parentPublicSegment) { if ( $segment.decoration ) { return $segment.decoration } $Uri = $segment.ToGraphUriFromEndpoint($parser.context.connection.GraphEndpoint.Graph, $parser.context.Version) $entity = $segment.graphElement |=> GetEntity $namespace = if ( $entity ) { $entity.namespace } else {'Null' } $namespaceDelimited = $namespace + '.' $resultTypeData = $segment.graphElement.GetResultTypeData() $parentSegment = $segment.parent $entityClass = if ( $entity ) { $entity.Type } else { 'Null' } $isCollection = if ( $entity ) { $entity.typeData.IsCollection -eq $true } else { $false } # Use the return type, which for vertices is the self, but # for edges is the vertex to which the self leads $fullTypeName = if ( $resultTypeData ) { $resultTypeData.TypeName } else { '' } $shortTypeName = if ( $fullTypeName.ToLower().StartsWith($namespaceDelimited.tolower()) ) { $fullTypeName.Substring($namespaceDelimited.length) } else { $fullTypeName } $parentPath = if ( $parentSegment ) { $parentSegment.ToGraphUri($null) } $relativeUri = $segment.ToGraphUri($null) $path = $::.GraphUtilities |=> ToLocationUriPath $parser.context $relativeUri $relationship = if ( $segment.isdynamic -or $entityClass -eq 'Singleton' ) { 'Data' } elseif ( $iscollection ) { 'Collection' } else { 'Direct' } $info = $this.__GetInfoField($isCollection, $segment.isDynamic, $entityClass, $false) # Seems like ScriptClass constants have a strange behavior when used as a typename here. # To work around this, use ToString() $result = [PSCustomObject] @{ PSTypeName = ($this.SegmentDisplayTypeName.tostring()) ParentPath = $parentPath Info = $info Name = $segment.name Relation = $relationship Collection = $isCollection Class = $entityClass Type = $shortTypeName TypeId = $fullTypeName Id = $segment.name Namespace = $namespace AbsoluteUri = $Uri GraphName = $parser.context.name GraphUri = $relativeUri Path = $path FullTypeName = $fullTypeName Version = $parser.context.version Endpoint = $parser.context.connection.graphEndpoint.Graph IsDynamic = $segment.isDynamic Parent = $ParentPublicSegment Details = $segment Content = $null Preview = $null } $segment |=> Decorate $result $result } function ToPublicSegmentFromGraphItem( $graphContext, $graphItem, $requestSegment ) { $typeInfo = $::.TypeUriHelper |=> InferTypeUriInfoFromRequestItem $requestSegment $graphItem $fullTypeName = $typeInfo.FullTypeName $typeComponents = $fullTypeName -split '\.' # Objects may actually be raw json, or even binary, depending # on callers specifying that they don't want objects, but the # raw content value from the Graph web response $Id = $graphItem | select -expandproperty id -erroraction ignore $itemName = if ( $Id ) { $Id } else { '[{0}]' -f $graphItem.Gettype().name } $itemId = if ( $graphItem -is [PSCustomObject] -and $graphItem.pstypenames.contains('GraphSegmentDisplayType') -and ($graphItem.Content) ) { $graphItem.Content.Id } else { $itemName } # Using ToString() here to work around a strange behavior where # PSTypeName does not cause type conversion $result = [PSCustomObject] @{ PSTypeName = $requestSegment.pstypename.tostring() ParentPath = $requestSegment.Path Info = $this.__GetInfoField($false, $true, 'EntityType', $true) Name = $itemName Relation = 'Direct' Collection = $false Class = 'EntityType' Type = $typeComponents[$typeComponents.length - 1] TypeId = $fullTypeName Id = $itemId Namespace = $requestSegment.Namespace AbsoluteUri = $typeInfo.AbsoluteUri GraphName = $graphContext.Name GraphUri = $typeinfo.GraphUri Path = $typeInfo.FullPath FullTypeName = $fullTypeName Version = $requestSegment.Version Endpoint = $requestSegment.Endpoint IsDynamic = $true Parent = $null Details = $null Content = $graphItem Preview = $this.__GetPreview($graphItem, $itemId) } if ( $fullTypeName -and $graphItem ) { GetNewObjectWithMetadata $graphItem $result } else { $result } } function ToPublicSegmentFromGraphResponseObject( $graphContext, $graphObject ) { $typeInfo = $::.TypeUriHelper |=> InferTypeUriInfoFromRequestItem $null $graphObject $absoluteUri = $null $locationUriPath = $null if ( $typeInfo.GraphUri ) { $absoluteUri = $graphContext.Connection.GraphEndpoint.Graph, $graphContext.Version, $typeInfo.GraphUri $locationUriPath = $::.GraphUtilities |=> ToLocationUriPath $graphContext $typeInfo.GraphUri } $fullTypeName = $typeInfo.FullTypeName $typeComponents = $fullTypeName -split '\.' # Objects may actually be raw json, or even binary, depending # on callers specifying that they don't want objects, but the # raw content value from the Graph web response $Id = $graphObject | select -expandproperty id -erroraction ignore $itemName = if ( $Id ) { $Id } elseif ( $fullTypeName ) { $fullTypeName } else { '[{0}]' -f $graphObject.GetType().Name } $itemId = if ( $graphObject -is [PSCustomObject] -and $graphObject.pstypenames.contains('GraphSegmentDisplayType') -and ($graphObject.Content) ) { $graphObject.Content.Id } else { $itemName } $namespace = if ( $fullTypeName ) { $components = $fullTypeName -split '\.' $length = (, $components).length -2 if ( $length -gt 0 ) { $components[0..$length] -join '.' } else { $fullTypeName } } # Using ToString() here to work around a strange behavior where # PSTypeName does not cause type conversion $result = [PSCustomObject] @{ PSTypeName = ($this.SegmentDisplayTypeName.tostring()) ParentPath = $null Info = $this.__GetInfoField($false, $true, 'EntityType', $true) Name = $itemName Relation = 'Direct' Collection = $false Class = 'EntityType' Type = $typeComponents[$typeComponents.length - 1] TypeId = $fullTypeName Id = $itemId Namespace = $namespace AbsoluteUri = $absoluteUri GraphName = $graphContext.Name GraphUri = $typeinfo.GraphUri Path = $locationUriPath FullTypeName = $fullTypeName Version = $graphContext.Version Endpoint = $graphContext.Connection.GraphEndpoint IsDynamic = $true Parent = $null Details = $null Content = $graphObject Preview = $this.__GetPreview($graphObject, $itemId) } if ( $fullTypeName -and $graphObject ) { GetNewObjectWithMetadata $graphObject $result } else { $result } } function AddContent($publicSegment, $content) { if ($publicSegment.content) { throw "Segment $($publicSegment.id) already has content" } if ($publicSegment.Preview) { throw "Segment $($publicSegment.id) already has a Preview" } if ( $content | gm id -erroraction ignore ) { $publicSegment.Id = $content.id } $publicSegment.content = $content $publicSegment.Preview = $this.__GetPreview($content, $publicSegment.name) $publicSegment.Info = $this.__GetInfoField($false, $true, 'EntityType', $true) } function GetNewObjectWithMetadata($graphItem, $segmentMetadata) { $wrappedObject = [AutoGraph.Model.GraphObject]::new($segmentMetadata) foreach ( $property in $graphItem.psobject.properties ) { $wrappedObject.psobject.properties.Add($property, $true) } $itemContext = $graphItem.psobject.methods | where Name -eq __ItemContext if ( $itemContext ) { $wrappedObject.psobject.methods.Add($itemContext[0], $true) } $itemTypeName = "AutoGraph.Entity.$($segmentMetadata.TypeId)" # When an item is returned as part of a heterogeneous collection, it should have # an '@odata.type'. In this case, to ensure that table formatting is sensible, # we lower the priority of the type so that it uses a more generic type that # shows less specific but common information for any type. $specificTypeIndex = if ( $graphItem | gm '@odata.type' -erroraction ignore ) { if ( $::.CustomFormatter.SupportsHeterogeneousFormatter($itemTypeName) ) { 0 } else { 1 } } else { 0 } $wrappedObject.pstypenames.insert(0, 'GraphResponseObject') $wrappedObject.pstypenames.insert(0, 'AutoGraph.Entity') $wrappedObject.pstypenames.insert($specificTypeIndex, $itemTypeName) $wrappedObject } function __GetPreview($content, $defaultValue) { $previewProperties = $content | select Name, DisplayName, Title, FileName, Subject, Topic, Id, bodyPreview if ( $previewProperties.Name ) { $previewProperties.Name } elseif ( $previewProperties.DisplayName ) { $previewProperties.DisplayName } elseif ( $previewProperties.Title ) { $previewProperties.Title } elseif ( $previewProperties.FileName ) { $previewProperties.FileName } elseif ( $previewProperties.Subject ) { $previewProperties.Subject } elseif ( $previewProperties.Topic ) { $previewProperties.Topic } elseif ( $previewProperties.bodyPreview ) { $previewproperties.bodyPreview } elseif ( $previewProperties.Id ) { $previewProperties.Id } else { $defaultValue } } function __GetInfoField($isCollection, $isDynamic, $entityClass, $hasContent) { $info = 0..3 $info[0] = $this.__EntityClassToSymbol($entityClass) $info[1] = if ( $isCollection ) { '*' } else { ' ' } $info[2] = if ( $hasContent ) { '+' } else { ' ' } $info[3] = if ( $this.IsValidLocationClass($entityClass ) ) { '>' } else { ' ' } $info -join '' } function __EntityClassToSymbol($entityClass) { switch ($entityClass) { 'EntityType' { 't' } 'NavigationProperty' { 'n' } 'EntitySet' { 'e' } 'Singleton' { 's' } 'Function' { 'f' } 'Action' { 'a' } 'Null' { '-' } '__Root' { '/' } default { '?' } } } function __RegisterSegmentDisplayType { remove-typedata -typename $this.SegmentDisplayTypeName -erroraction ignore $coreProperties = @('Info', 'Type', 'Preview', 'Id') $segmentDisplayTypeArguments = @{ TypeName = $this.segmentDisplayTypeName MemberType = 'NoteProperty' MemberName = 'PSTypeName' Value = $this.SegmentDisplayTypeName DefaultDisplayPropertySet = $coreProperties } Update-TypeData -force @segmentDisplayTypeArguments } } } $::.SegmentHelper |=> __initialize |