src/cmdlets/common/TypeUriHelper.ps1
# Copyright 2020, 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. ScriptClass TypeUriHelper { static { const TYPE_METHOD_NAME __ItemType function DefaultUriForType($targetContext, $entityTypeName) { $entitySet = $::.GraphManager |=> GetGraph $targetContext |=> GetEntityTypeToEntitySetMapping $entityTypeName if ( $entitySet ) { [Uri] "/$entitySet" } } function TypeFromUri([Uri] $uri, $targetContext) { $uriInfo = Get-GraphUriInfo $Uri -GraphName $targetContext.name -erroraction stop [PSCustomObject] @{ FullTypeName = $uriInfo.FullTypeName IsCollection = $uriInfo.Collection UriInfo = $uriInfo } } function DecorateObjectWithType($graphObject, $typeName) { $graphObject | add-member -membertype ScriptMethod -name $this.TYPE_METHOD_NAME -value ([ScriptBlock]::Create("'$typeName'")) } function GetUriFromDecoratedResponseObject($targetContext, $responseObject, $resourceId) { # This method handles two cases: # # * Objects returned by Get-GraphResource which are decorated with the __ItemContext scriptmethod # * Objects returned by Get-GraphResourceWithMetadata which are PSCustomObjects of GraphSgementDisplayType # # The latter has a Path member with exactly the uri needed to resolve the object, the other requires # a workaround since it may only have a partial URI originally used as the target of a POST that created it. if ( ( $responseObject -is [PSCustomObject] ) -and ( $responseObject.psobject.typenames -contains 'GraphSegmentDisplayType' ) ) { $responseObject.GraphUri.tostring() } else { # Try to parse the odata context $itemContext = if ( $responseObject | gm -membertype scriptmethod __ItemContext -erroraction ignore ) { $responseObject.__ItemContext() } # The IsCollectionMember property of itemContext cannot always be trusted -- for our use case # we ignore this for entities. TODO: Address this in the context itself so we can actually trust the property $objectUri = if ( ! $itemContext -or ! ( $itemContext.IsEntity -and $itemContext.IsCollectionMember ) ) { $::.GraphUtilities |=> GetAbstractUriFromResponseObject $responseObject $true $resourceId } # If the odata context is not parseable for some reason or we do not trust it, fall back to older and slower logic if ( ! $objectUri -and $itemContext ) { $requestUri = $::.GraphUtilities |=> ParseGraphUri $itemContext.RequestUri $targetContext $objectUri = $requestUri.GraphRelativeUri $uriInfo = if ( $resourceId ) { Get-GraphUriInfo $objectUri } # When an object is supplied, its URI had better end with whatever id was supplied. # This will not always be true of the uri retrieved from the object because this URI is the # URI that was used to request the object from Graph, not necessarily the object's actual # URI. For example, a request to POST to /groups will return an object located at # /groups/9318e52c-6cd7-430e-9095-a54aa5754381. But __ItemContext contains the URI that was # used to make the POST request, i.e. /groups. However, since the id is supplied to this method, # we can recover the URI if we assume the discrepancy is indeed due to this scenario. # TODO: Get an explicit object URI from the object itself rather than this workaround which # will have problematic corner cases. if ( $uriInfo -and $uriInfo.Collection -and $resourceId -and ! $objectUri.tostring().tolower().EndsWith("/$($resourceId.tolower())") ) { $objectUri = $objectUri.tostring(), $resourceId -join '/' } } if ( ! $objectUri ) { throw 'Unable to determine the Graph URI for the specified object' } $objectUri.tostring() } } function GetTypeFromDecoratedObject($graphObject) { if ( $graphObject | gm -membertype scriptmethod $this.TYPE_METHOD_NAME -erroraction ignore ) { $graphObject.($this.TYPE_METHOD_NAME)() } } function InferTypeUriInfoFromRequestItem($requestItem, $responseObject) { $absoluteUri = $null $fullPath = $null $graphUri = $null $typeSpecifier = $::.GraphUtilities |=> GetOptionalTypeFromResponseObject $responseObject $fullTypeName = if ( $typeSpecifier ) { $typeData = $::.GraphUtilities.ParseTypeName($typeSpecifier) $typeData.TypeName } if ( $requestItem ) { $absoluteUri = $requestItem.AbsoluteUri $fullPath = $requestItem.Path $graphUri = $requestItem.GraphUri if ( ! $fullTypeName ) { $fullTypeName = $requestItem.FullTypeName } if ( $requestItem.Collection ) { $absoluteUri = $absoluteUri.trimend('/'), $responseObject.Id -join '/' $fullPath = $fullPath.trimend('/'), $responseObject.Id -join '/' $graphUri = [Uri] ($graphUri.tostring().trimend('/'), $responseObject.Id -join '/') } } else { $graphUri = $::.GraphUtilities |=> GetAbstractUriFromResponseObject $responseObject $true } [PSCustomObject] @{ FullTypeName = $fullTypeName AbsoluteUri = $absoluteUri FullPath = $fullPath GraphUri = $graphUri } } function GetUriFromDecoratedObject($targetContext, $graphObject, $noInterpolation = $false) { $idHint = if ( ! $noInterpolation -and ( $graphObject | gm id -erroraction ignore ) ) { $graphObject.id } $objectUri = GetUriFromDecoratedResponseObject $targetContext $graphObject $idHint if ( ! $objectUri ) { $type = GetTypeFromDecoratedObject $graphObject if ( $type ) { $objectUri = DefaultUriForType $targetContext $type } } $objectUri } function GetTypeAwareRequestInfo($graphName, $typeName, $fullyQualifiedTypeName, $uri, $id, $typedGraphObject, $ignoreTypeIfObjectPresent, $targetUriOptional) { $metadata = if ( $typedGraphObject -and ( $typedGraphObject | gm __ItemMetadata -erroraction ignore ) ) { $typedGraphObject.__ItemMetadata() } $targetGraphName = if ( $metadata ) { $metadata.Graphname } else { $graphName } $targetContext = $::.ContextHelper |=> GetContextByNameOrDefault $targetGraphName $targetUri = if ( $uri ) { $::.GraphUtilities |=> ToGraphRelativeUri $uri $targetContext } elseif ( $metadata ) { $metadata.GraphUri } $targetTypeInfo = if ( $typeName -and ( ! $typedGraphObject -or $ignoreTypeIfObjectPresent ) ) { $remappedTypeClass = if ( $targetUriOptional ) { 'Any' } else { # We only need to enforce entity if we expect a default URI 'Entity' } $resolvedType = Get-GraphType $TypeName -TypeClass $remappedTypeClass -GraphName $targetContext.Name -FullyQualifiedTypeName:$fullyQualifiedTypeName -erroraction stop $typeUri = DefaultUriForType $targetContext $resolvedType.TypeId if ( $typeUri ) { $targetUri = $typeUri, $id -join '/' } elseif ( ! $targetUriOptional ) { throw "Unable to find URI for type '$typeName' -- explicitly specify the target URI or an existing item and retry." } [PSCustomObject] @{ FullTypeName = $resolvedType.typeId IsCollection = $true } } elseif ( $uri -and ! ( $ignoreTypeIfObjectPresent -and $typedGraphObject ) ) { # TODO: just increase precedence of metadata (i.e. typedGraphObject) over uri TypeFromUri $targetUri $targetContext } elseif ( $typedGraphObject ) { if ( $metadata -and $::.SegmentHelper.IsGraphSegmentType($metadata) ) { # This is already a fully described object -- no need to make expensive # calls to parse metadata and understand the object $objectUri = $metadata.GraphUri $targetUri = $objectUri [PSCustomObject] @{ FullTypeName = $metadata.FullTypeName IsCollection = $metadata.Collection UriInfo = $metadata } } else { # We need to analyze information about the object using its uri since we # don't have existing information -- this is expensive, so hopefully # it doesn't occur to often $objectUri = GetUriFromDecoratedObject $targetContext $typedGraphObject $id if ( $objectUri ) { $objectUriInfo = TypeFromUri $objectUri $targetContext # TODO: When an object is supplied, it had better end with whatever id was supplied. # This will not always be true of the uri retrieved from the object because of some # corner cases with the commands used to get objects from the graph, particularly # when an object is retrieved as part of a collection URI -- such URIs do not # contain an id, they end with the parent segment. Another case where this happens # is if the object was created through a POST, though that should definitely be # fixed in the command that creates objects. if ( $id -and ! $objectUri.tostring().tolower().EndsWith("/$($id.tolower())" ) ) { if ( $objectUriInfo.UriInfo.class -in 'EntityType', 'EntitySet' ) { $correctedUri = $objectUri, $id -join '/' $objectUriInfo = TypeFromUri $correctedUri $targetContext } elseif ( $objectUriInfo.UriInfo.class -ne 'Singleton' ) { # TODO: Refine this condition to avoid possibly invalid assumptions. # The object was probably obtained via POST or by enumerating an object collection, # so we'll just assume it's safe to concatenate the id. However, once the corner cases # are corrected in the object decoration, we should update to reliable logic. $itemContext = if ( $typedGraphObject | Get-Member -MemberType ScriptMethod __ItemContext -erroraction ignore ) { $typedGraphObject.__ItemContext() } $assumeNotCollectionMember = if ( $itemContext ) { $itemContext.IsEntity -and $itemContext.IsCollectionMember } # Detect the case where we have a navigation to a single entity (not a collection # that contained this element) if ( ! $assumeNotCollectionMember ) { $correctedUri = $objectUri, $id -join '/' $objectUriInfo = TypeFromUri $correctedUri $targetContext } } } $targetUri = $objectUriInfo.UriInfo.graphUri $objectUriInfo } } } $targetUriString = if ( $targetUri ) { $targetUri.tostring().trimend('/') } elseif ( ! $targetUriOptional ) { throw [ArgumentException]::new('Either a type name or URI must be specified') } [PSCustomObject] @{ Context = $targetContext TypeName = $targetTypeInfo.FullTypeName IsCollection = $targetTypeInfo.IsCollection TypeInfo = $targetTypeInfo Uri = $targetUriString } } function ToGraphAbsoluteUri($targetContext, [Uri] $graphRelativeUri) { $uriString = $targetContext.connection.graphendpoint.graph.tostring().trimend('/'), $targetContext.version, $graphRelativeUri.tostring().trimstart('/') -join '/' [Uri] $uriString } function GetReferenceSourceInfo($graphName, $typeName, $isFullyQualifiedTypeName, $id, $uri, $graphObject, $navigationProperty, $relationshipInfo) { $fromId = if ( $Id ) { $Id } elseif ( $graphObject -and ( $graphObject | gm -membertype noteproperty id -erroraction ignore ) ) { $graphObject.Id # This is needed when an object is supplied without an id parameter } $requestInfo = if ( $relationshipInfo ) { $::.TypeUriHelper |=> GetTypeAwareRequestInfo $relationshipInfo.GraphName $null $false $relationshipInfo.FromUri $null $null } else { $::.TypeUriHelper |=> GetTypeAwareRequestInfo $GraphName $TypeName $isFullyQualifiedTypeName $uri $fromId $graphObject } $segments = @() $segments += $requestInfo.uri.tostring() $targetNavigation = if ( $RelationshipInfo ) { $relationshipInfo.Relationship } else { $navigationProperty } if ( $targetNavigation -and $requestInfo.uri ) { $segments += $targetNavigation } $sourceUri = $segments -join '/' [PSCustomObject] @{ Uri = $sourceUri RequestInfo = $requestInfo } } function GetReferenceTargetTypeInfo($graphName, $requestInfo, $navigationProperty, $overrideTargetTypeName, $allowCollectionTarget) { $targetTypeName = $OverrideTargetTypeName $isCollection = $false if ( $navigationProperty ) { $targetPropertyInfo = if ( ! $OverrideTargetTypeName -or $allowCollectionTarget ) { $targetType = Get-GraphType -GraphName $graphName $requestInfo.TypeName $targetTypeInfo = $targetType.Relationships | where name -eq $navigationProperty if ( ! $targetTypeInfo ) { return $null } $isCollection = $targetTypeInfo.IsCollection $targetTypeInfo } if ( ! $targetTypeName ) { $targetTypeName = $targetPropertyInfo.TypeId } } [PSCustomObject] @{ TypeId = $targetTypeName IsCollectionTarget = $isCollection } } function GetReferenceTargetInfo($graphName, $targetTypeName, $isFullyQualifiedTypeName, $targetId, $targetUri, $targetObject, $allowCollectionTarget = $false, $relationshipInfo) { if ( $relationshipInfo ) { $::.TypeUriHelper |=> GetTypeAwareRequestInfo $relationshipInfo.GraphName $null $false $relationshipInfo.TargetUri $relationshipInfo.TargetId $null } elseif ( $TargetUri ) { foreach ( $destinationUri in $TargetUri ) { $::.TypeUriHelper |=> GetTypeAwareRequestInfo $GraphName $null $false $destinationUri $null $null } } elseif ( $TargetObject ) { $targetObjectId = if ( $TargetObject | gm id -erroraction ignore ) { $TargetObject.id } else { throw "An object specified for the 'TargetObject' parameter does not have an Id field; specify the object's URI or the TypeName and Id parameters and retry the command" } # The assumption here is that anything that can be a target must be able to be referenced as part of an entityset. # This generally seems to be true. $::.TypeUriHelper |=> GetTypeAwareRequestInfo $graphName $targetTypeName $isFullyQualifiedTypeName $null $targetObjectId $null } else { foreach ( $destinationId in $targetId ) { $::.TypeUriHelper |=> GetTypeAwareRequestInfo $GraphName $targetTypeName $isFullyQualifiedTypeName $null $destinationId $null $false } } } } } |