src/metadata/GraphBuilder.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 GraphDataModel) . (import-script EntityEdge) . (import-script EntityVertex) enum BuildFlags { NavigationsProcessed = 1 MethodsProcessed = 2 CopiedToSingleton = 4 } ScriptClass GraphBuilder { $graphEndpoint = $null $version = $null $dataModel = $null static { $AllBuildFlags = ([BuildFlags]::NavigationsProcessed) -bOR ([BuildFlags]::MethodsProcessed) -bOR ([BuildFlags]::CopiedToSingleton) } function __initialize($graphEndpoint, $version, $dataModel) { $this.graphEndpoint = $graphEndpoint $this.version = $version $this.dataModel = $dataModel } function InitializeGraph($graph) { $metadataActivity = "Building graph version '$($this.version)' for endpoint '$($this.graphEndpoint)'" Write-Progress -id 2 -activity $metadataActivity -ParentId 1 __AddRootVertices $graph Write-Progress -id 2 -activity $metadataActivity -Completed -ParentId 1 } function __AddEntityTypeVertex($graph, $typeName) { AddEntityTypeVertices $graph $typeName } function __AddRootVertices($graph) { $singletons = $this.dataModel |=> GetSingletons __AddVerticesFromSchemas $graph $singletons Singleton $entitySets = $this.dataModel |=> GetEntitySets __AddVerticesFromSchemas $graph $entitySets EntitySet } function __AddVerticesFromSchemas($graph, $schemas, $vertexType) { $schemas | foreach { if ( ! $_.namespace ) { throw "No namespace specified for schema '$($_.QualifiedName)' of vertex type '$vertexType'" } __AddVertex $graph $_.Schema $vertexType $_.namespace } } function __AddVertex($graph, $schema, $vertexType, $namespace, $namespaceAlias) { $entity = new-so Entity $schema $namespace $graph.AddVertex($entity) } function AddEntityTypeVertices($graph, $qualifiedTypeName) { $entityType = $this.dataModel.GetEntityTypeByName($qualifiedTypeName) if ( $qualifiedTypeName -and $entityType -eq $null ) { throw "Type '$qualifiedTypeName' does not exist in the schema for the graph at endpoint '$($graph.endpoint)' with API version '$($graph.apiversion)'" } Write-Progress -id 1 -activity "Adding type '$qualifiedTypeName'" __AddVerticesFromSchemas $graph $entityType EntityType } function __AddEdgesToEntityTypeVertex($graph, $sourceVertex) { if ( $sourceVertex.TestFlags([BuildFlags]::NavigationsProcessed) ) { return } $transitions = if ( $sourceVertex.entity.navigations ) { $sourceVertex.entity.navigations } else { @() } foreach ( $transition in $transitions ) { # Look for the existing type in the graph itself $unaliasedName = $this.dataModel.UnAliasQualifiedName($transition.typedata.typename) $sink = $graph.TypeVertexFromTypeName($unaliasedName) if ( ! $sink ) { # If we don't find the existing type, try to get it from the model instead $sinkSchema = $this.dataModel.GetEntityTypeByName($unAliasedName) if ( $sinkSchema ) { # We've found the type, now add it to the graph __AddEntityTypeVertex $graph $unaliasedName $sink = $graph.TypeVertexFromTypeName($unaliasedName) } else { write-verbose "Unable to find schema for '$($transition.type)', $($transition.typedata.typename)" } } if ( $sink ) { $edge = new-so EntityEdge $sourceVertex $sink $transition $sourceVertex.AddEdge($edge) } else { write-verbose "Unable to find entity type for '$($transition.type)', $($transition.typedata.typename) = '$unaliasedName', skipping" } } $sourceVertex.SetFlags([BuildFlags]::NavigationsProcessed) } function AddEdgesToVertex($graph, $vertex, $skipIfExist) { if ( $vertex.TestFlags($::.GraphBuilder.AllBuildFlags) ) { if ( !$skipIfExist ) { throw "Vertex '$($vertex.name)' already has edges" } return } $qualifiedTypeName = $vertex.entity.typedata.typename $unqualifiedTypeName = $this.dataModel.UnqualifyTypeName($qualifiedTypeName) Write-Progress -id 1 -activity "Adding edges for '$($vertex.name)'" __AddEdgesToEntityTypeVertex $graph $vertex if ( $vertex.entity.type -ne 'Singleton' ) { __AddMethodTransitionsToVertex $graph $vertex } else { __CopyEntityTypeEdgesToSingletonVertex $graph $vertex } } function __CopyEntityTypeEdgesToSingletonVertex($graph, $source) { if ( $source.TestFlags([BuildFlags]::CopiedToSingleton) ) { throw "Data from type already copied to singleton '$($source.name)'" } $entityName = ($source.entity.typeData).TypeName $typeVertex = $graph.TypeVertexFromTypeName($entityName) if ( $typeVertex -eq $null ) { throw "Unable to find an entity type for singleton '$($_.name)' and '$entityName'" } AddEdgesToVertex $graph $typeVertex $true $edges = $typeVertex.outgoingEdges.values | foreach { if ( ( $_ | gm transition -erroraction ignore ) -ne $null ) { $_ } } $edges | foreach { $sink = $_.sink $transition = $_.transition $edge = new-so EntityEdge $source $sink $transition $source.AddEdge($edge) } $source.SetFlags([BuildFlags]::CopiedToSingleton) } function __AddMethodTransitionsToVertex($graph, $sourceVertex) { if ( $sourceVertex.TestFlags([BuildFlags]::MethodsProcessed) ) { write-verbose "Methods already processed for $($sourceVertex.name), skipping method addition" return } $sourceTypeName = $sourceVertex.entity.typeData.TypeName $methodBindings = $this.dataModel.GetMethodBindingsForType($sourceTypeName) if ( ! $methodBindings ) { write-verbose "Vertex ($sourceVertex.name) has no methods, skipping method addition" return } $methodBindings.Schema | foreach { $method = $_ $sink = if ( $method | gm ReturnType ) { # If there's a return type, it can actually be of any type, not just an entity # type. We'll link this to a vertex for the entity type if it's an entity type, but # if the return type is not an entity, we'll just link it to a single "scalar" vertex, # or the null vertex if there is no return type. $typeName = $method.ReturnType | select -expandproperty Type $parsedName = $::.GraphUtilities.ParseTypeName($typeName) $unaliasedName = $this.dataModel.UnAliasQualifiedName($parsedName.TypeName) # This only returns vertices (i.e. entity types) that have already been seen, # so we may not find it. $typeVertex = $graph.TypeVertexFromTypeName($unaliasedName) if ( $typeVertex -eq $null ) { # If the return type is not found, then see if such an entity type exists. # If it doesn't, that means the return type is not an entity, i.e. # it is a primitive, enumeration, or complex type. In this context, # we will treat these as "scalar" types -- they are not traversable. if ( $this.dataModel.GetEntityTypeByName($unaliasedName) ) { try { __AddEntityTypeVertex $graph $unaliasedName $typeVertex = $graph.TypeVertexFromTypeName($unaliasedName) } catch { # The scheme is malformed such that even though the return type # is listed in the schema, we could find no vertex. Move on from # this procesing error, future attempts to traverse the return # type will not succeeds. } } } if ( $typeVertex ) { $typeVertex } else { write-verbose "Type $($typeName) returned by $($method.name) cannot be found, configuring Scalar vertex" $::.EntityVertex.ScalarVertex } } else { $::.Entityvertex.NullVertex } __AddMethod $sourceVertex $method $sink } $sourceVertex.SetFlags([BuildFlags]::MethodsProcessed) } function __AddMethod($targetVertex, $methodSchema, $returnTypeVertex) { if ( ! ($targetVertex.EdgeExists($methodSchema.name)) ) { $nameInfo = __GetNamespaceInfoFromQualifiedTypeName $targetVertex.typeName $methodEntity = new-so Entity $methodSchema $nameInfo.Namespace $edge = new-so EntityEdge $targetVertex $returnTypeVertex $methodEntity $targetVertex.AddEdge($edge) } else { write-verbose "Skipped add of edge $($methodSchema.name) to $($returnTypeVertex.id) from vertex $($targetVertex.id) because it already exists." } } function __GetNamespaceInfoFromQualifiedTypeName($qualifiedTypeName) { $nameInfo = $this.dataModel.ParseTypeName($qualifiedTypeName, $true) $namespaceAlias = if ( $nameInfo.Namespace ) { $this.dataModel.GetNamespaceAlias($nameInfo.namespace) } [PSCustomObject] @{ Namespace = $nameInfo.Namespace NamespaceAlias = $namespaceAlias } } } |