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 -GraphScope $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 | gm -membertype scriptmethod __ItemContext -erroraction ignore ) { $requestUri = $::.GraphUtilities |=> ParseGraphUri $responseObject.__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 '/' } $objectUri.tostring() } elseif ( ( $responseObject -is [PSCustomObject] ) -and ( $responseObject.psobject.typenames -contains 'GraphSegmentDisplayType' ) ) { $responseObject.GraphUri.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 = $requestItem.Uri $fullPath = $requestItem.Path $graphUri = $requestItem.GraphUri if ( $requestItem.Collection ) { $absoluteUri = $absoluteUri.trimend('/'), $responseObject.Id -join '/' $fullPath = $fullPath.trimend('/'), $responseObject.Id -join '/' $graphUri = [Uri] ($graphUri.tostring().trimend('/'), $responseObject.Id -join '/') } [PSCustomObject] @{ FullTypeName = $requestItem.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) { $targetContext = $::.ContextHelper |=> GetContextByNameOrDefault $graphName $targetUri = if ( $uri ) { $::.GraphUtilities |=> ToGraphRelativeUri $uri $targetContext } $targetTypeInfo = if ( $typeName ) { $resolvedType = Get-GraphType $TypeName -TypeClass Entity -GraphName $graphName -FullyQualifiedTypeName:$fullyQualifiedTypeName -erroraction stop $typeUri = DefaultUriForType $targetContext $resolvedType.TypeId if ( $typeUri ) { $targetUri = $typeUri, $id -join '/' } else { 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 ) { TypeFromUri $targetUri $targetContext } elseif ( $typedGraphObject ) { if ( $::.SegmentHelper |=> IsGraphSegmentType $typedGraphObject ) { # This is already a fully described object -- no need to make expensive # calls to parse metadata and understand the object $objectUri = $typedGraphObject.GraphUri $targetUri = $objectUri [PSCustomObject] @{ FullTypeName = $typedGraphObject.FullTypeName IsCollection = $typedGraphObject.Collection UriInfo = $typedGraphObject } } 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 if ( $id -and ( $objectUriInfo.UriInfo.class -in ( 'EntityType', 'EntitySet' ) ) -and ! $objectUri.tostring().tolower().EndsWith("/$($id.tolower())" ) ) { $correctedUri = $objectUri, $id -join '/' $objectUriInfo = TypeFromUri $correctedUri $targetContext } $targetUri = $objectUriInfo.UriInfo.graphUri $objectUriInfo } } } if ( ! $targetUri ) { throw [ArgumentException]::new('Either a type name or URI must be specified') } [PSCustomObject] @{ Context = $targetContext TypeName = $targetTypeInfo.FullTypeName IsCollection = $targetTypeInfo.IsCollection TypeInfo = $targetTypeInfo Uri = $targetUri.tostring().trimend('/') } } 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) { $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 = $::.TypeUriHelper |=> GetTypeAwareRequestInfo $GraphName $TypeName $isFullyQualifiedTypeName $uri $fromId $GraphObject $segments = @() $segments += $requestInfo.uri.tostring() if ( $navigationProperty -and $requestInfo.uri ) { $segments += $navigationProperty } $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) { if ( $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 $destinationId $null $null } } } } } |