src/metadata/GraphDataModel.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. . (import-script QualifiedSchema) ScriptClass GraphDataModel { $apiMetadata = $null $apiModels = $null $aliases = $null $methodBindings = $null $typeSchemas = $null static { const DefaultNamespace 'microsoft.graph' } function __initialize($apiMetadata) { $this.apiMetadata = $apiMetadata $this.apiModels = @{} $this.aliases = @{} $apiMetadata.Edmx.DataServices.Schema | foreach { $schema = $_ $namespace = $schema.Namespace if ( $this.apiModels[$namespace] ) { throw "Namespace '$namespace' already exists as a namespace or alias" } $alias = $null # For more information about namespace aliases, see the following OData documentation: # https://www.odata.org/documentation/odata-version-3-0/common-schema-definition-language-csdl/ if ( $schema | gm Alias -erroraction ignore ) { $alias = $schema.alias if ( $this.aliases[$alias] -or $this.apiModels[$alias] ) { throw "Alias '$alias' already exists as a namespace or alias" } $this.aliases.add($alias, $namespace) } $model = [PSCustomObject] @{ Namespace = $namespace Alias = $alias Schema = $schema } $this.apiModels.add($namespace, $model) } } function GetDefaultNamespace { $this.scriptclass.DefaultNamespace } function GetNamespaces { $this.apiModels.keys } function GetSchema($namespace) { if ( ! $this.apiModels[$namespace] ) { throw "No model could be found for the specified namespace '$namespace'" } $this.apiModels[$namespace].Schema } function GetEntityTypeByName($typeName) { __InitializeTypesOnDemand $this.typeSchemas[$typeName] } function GetMethodBindingsForType($typeName) { if ( $this.methodBindings -eq $null ) { $this.methodBindings = @{} $actions = GetActions __AddMethodBindingsFromMethodSchemas $actions $functions = GetFunctions __AddMethodBindingsFromMethodSchemas $functions } $bindings = $this.methodBindings[$typeName] if ( $bindings ) { $bindings.Distinct.Values } } function GetEntityTypes { __InitializeTypesOnDemand $this.typeSchemas.Values } function GetComplexTypes($typeName) { foreach ( $model in $this.apiModels.Values ) { if ( $model.Schema | gm ComplexType -erroraction ignore ) { $complexTypeSchemas = if ( $typeName ) { $model.Schema.ComplexType | where Name -eq $typeName } else { $model.Schema.ComplexType } foreach ( $complexType in $complexTypeSchemas ) { new-so QualifiedSchema ComplexType $model.namespace $complexType.name $complexType } } } } function GetEntitySets { Write-Progress -id 1 -activity "Reading entity sets" foreach ( $model in $this.apiModels.Values ) { if ( $model.Schema | gm EntityContainer -erroraction ignore ) { __QualifySchemaClass EntitySet $model.namespace $model.Schema.EntityContainer } } } function GetSingletons { Write-Progress -id 1 -activity "Reading singletons" foreach ( $model in $this.apiModels.Values ) { if ( $model.Schema | gm EntityContainer -erroraction ignore ) { __QualifySchemaClass Singleton $model.namespace $model.Schema.EntityContainer } } } function GetEnumTypes { Write-Progress -id 1 -activity "Reading enumerated types" foreach ( $model in $this.apiModels.Values ) { __QualifySchemaClass EnumType $model.namespace $model.Schema } } function GetActions { Write-Progress -id 1 "Reading actions" foreach ( $model in $this.apiModels.Values ) { __QualifySchemaClass Action $model.namespace $model.Schema } } function GetFunctions { Write-Progress -id 1 "Reading functions" foreach ( $model in $this.apiModels.Values ) { __QualifySchemaClass Function $model.namespace $model.Schema } } function UnqualifyTypeName($qualifiedTypeName) { (ParseTypeName $qualifiedTypeName).UnqualifiedName } function ParseTypeName($qualifiedTypeName, $onlyIfQualified = $false) { $prefix = '' $namespace = $null $unqualified = if ( $qualifiedTypeName.Contains('.') ) { $lastElement = $null $qualifiedTypeName -split '\.' | foreach { $lastElement = $_ if ( $prefix.length + 1 + $_.length -lt $qualifiedTypeName.length ) { if ( $prefix.length -gt 0 ) { $prefix += '.' } $prefix += "$_" } } if ( $this.apiModels[$prefix] ) { $namespace = $prefix $lastElement } elseif ( $this.aliases[$prefix] ) { $namespace = $this.aliases[$prefix] $lastElement } } $nameResult = if ( $unqualified ) { $unqualified } elseif ( ! $onlyIfQualified ) { $qualifiedTypeName } [PSCustomObject] @{ Namespace = $namespace UnqualifiedName = $nameResult } } function UnaliasQualifiedName($name) { $nameInfo = ParseTypeName $name $true if ( $nameInfo.namespace ) { $nameInfo.namespace, $nameInfo.UnqualifiedName -join '.' } else { $name } } function GetNamespaceAlias($namespace) { $this.aliases[$namespace] } function __InitializeTypesOnDemand { if ( ! $this.typeSchemas ) { $this.typeSchemas = @{} Write-Progress -id 1 -activity "Reading entity types" foreach ( $model in $this.apiModels.Values ) { if ( $model.Schema | gm EntityType -erroraction ignore ) { $typeSchemas = $model.Schema.EntityType $typeSchemas | foreach { $qualifiedSchema = new-so QualifiedSchema EntityType $model.namespace $_.name $_ $this.typeSchemas.Add($qualifiedSchema.qualifiedName, $qualifiedSchema) } } } Write-Progress -id 1 -activity "Reading entity types" -completed } } function __QualifySchemaClass($schemaClass, $namespace, $schemaContainer) { if ( $schemaContainer | gm $schemaClass -erroraction ignore ) { foreach ( $schemaElement in $schemaContainer.$schemaClass ) { new-so QualifiedSchema $schemaClass $namespace $schemaElement.name $schemaElement } } } function __AddMethodBindingsFromMethodSchemas($methodSchemas) { if ( $methodSchemas ) { # Assume that the first parameter is actually the binding parameter -- it is usually, # but not always, named 'bindingParameter' foreach ( $methodSchema in $methodSchemas ) { $nativeSchema = $methodSchema.Schema if ( ( $nativeSchema | gm parameter -erroraction ignore ) -and $nativeSchema.parameter) { $bindingParameter = $nativeSchema.parameter | where name -eq 'bindingParameter' | select -first 1 if ( ! $bindingParameter ) { $bindingParameter = $nativeSchema.parameter | select -first 1 } if ( $bindingParameter ) { __AddMethodBinding $bindingParameter.type $nativeSchema } } } } } function __AddMethodBinding($typeName, $methodSchema) { $unaliasedName = UnaliasQualifiedName $typeName if ( $this.methodBindings[$unaliasedName] -eq $null ) { $this.methodBindings[$unaliasedName] = @{Distinct=@{};All=@()} } # TODO: Understand this strange behavior seen in real metadata. # This covers a strange case -- it turns out that in 2021-02-22, # driveItem has not one, but two function elements for getactivitybyinterval. # The only difference is that one has no parameters, the other one does. # From documentation (https://docs.microsoft.com/en-us/graph/api/itemactivitystat-getactivitybyinterval?view=graph-rest-1.0&tabs=http), # there is nothing special about this function that would warrant such an odd # representation in the schema. The current theory is that the second instance, # which is the one that has the parameters, is the "real" instance, the first # is some strange schema generation artifact, possibly one that is ignored by # Microsoft Graph in some sort of "last writer wins" behavior. $this.methodBindings[$unaliasedName]['Distinct'][$methodSchema.Name] = $methodSchema $this.methodBindings[$unaliasedName]['All'] += $methodSchema } } |