Private/_Helpers.ps1

function Get-ManifestValue {
    #.Synopsis
    # Reads a specific value from a module manifest
    #.Description
    # By default Get-ManifestValue gets the ModuleVersion, but it can read any key in the Module Manifest, including the PrivateData, or the PSData inside the PrivateData.
    #.Example
    # Get-ManifestValue .\Configuration.psd1
    #
    # Returns the module version number (as a string)
    #.Example
    # Get-ManifestValue .\Configuration.psd1 ReleaseNotes
    #
    # Returns the release notes!
    [CmdletBinding()]
    param(
        # The path to the module manifest file
        [Parameter(ValueFromPipelineByPropertyName = "True", Position = 0)]
        [Alias("PSPath")]
        [string]$Manifest,

        # The property to be read from the manifest. Get-ManifestValue searches the Manifest root properties, then the properties PrivateData, then the PSData
        [Parameter(ParameterSetName = "Overwrite", Position = 1)]
        [string]$PropertyName = 'ModuleVersion',

        [switch]$Passthru
    )
    $ErrorActionPreference = "Stop"

    if (Test-Path $Manifest) {
        $ManifestContent = Get-Content $Manifest -Raw
    }
    else { 
        $ManifestContent = $Manifest
    }

    $Tokens = $Null; $ParseErrors = $Null
    $AST = [System.Management.Automation.Language.Parser]::ParseInput( $ManifestContent, $Manifest, [ref]$Tokens, [ref]$ParseErrors )
    $ManifestHash = $AST.Find( { $args[0] -is [System.Management.Automation.Language.HashtableAst] }, $true )
    $KeyValue = $ManifestHash.KeyValuePairs.Where{ $_.Item1.Value -eq $PropertyName }.Item2

    # Recursively search for PropertyName in the PrivateData and PrivateData.PSData
    if (!$KeyValue) {
        $global:devops_PrivateData = $ManifestHash.KeyValuePairs.Where{ $_.Item1.Value -eq 'PrivateData' }.Item2.PipelineElements.Expression
        $KeyValue = $PrivateData.KeyValuePairs.Where{ $_.Item1.Value -eq $PropertyName }.Item2
        if (!$KeyValue) {
            $global:devops_PSData = $PrivateData.KeyValuePairs.Where{ $_.Item1.Value -eq 'PSData' }.Item2.PipelineElements.Expression
            $KeyValue = $PSData.KeyValuePairs.Where{ $(Write-Verbose "'$($_.Item1.Value)' -eq '$PropertyName'"); $_.Item1.Value -eq $PropertyName }.Item2
            if (!$KeyValue) {
                Write-Error "Couldn't find '$PropertyName' to update in '$(Convert-Path $ManifestPath)'"
                return
            }
        }
    }

    if ($Passthru) { $KeyValue } else { $KeyValue.SafeGetValue() }
}

### Internal Helpers
function CrmTimerStart{
    $script:crmtimer = New-Object -TypeName 'System.Diagnostics.Stopwatch'
    $script:crmtimer.Start()
}

function CrmTimerStop{
    $crmtimerobj = Get-Variable crmtimer -Scope Script
    if($crmtimerobj.Value -ne $null)
    {
        $script:crmtimer = $crmtimerobj.Value
        $script:crmtimer.Stop()
        $perf = "The operation took " + $script:crmtimer.Elapsed.ToString()
        Remove-Variable crmtimer  -Scope Script
        return $perf
    }
}

function Convert-CrmEntityToPsObject {
    [CmdletBinding()]
    PARAM(
        [parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.Entity]$Entity,

        [parameter(Mandatory=$false)]
        [string]$LogicalName
    )

    $record = New-Object PSObject

    $record | Add-Member -MemberType NoteProperty -Name "Id" -Value $Entity.Id
    $record | Add-Member -MemberType NoteProperty -Name "LogicalName" -Value $Entity.LogicalName

    if(-not [string]::IsNullOrEmpty($LogicalName) -and $Entity.LogicalName -ne $LogicalName) {
        $record | Add-Member -MemberType NoteProperty -Name "RequestedLogicalName" -Value $LogicalName
    }

    foreach($attribute in $Entity.Attributes.GetEnumerator()) {
        $name = $attribute.Key
        $value = $attribute.Value

        if($value -is [Microsoft.Xrm.Sdk.AliasedValue]) {
            $value = $value.Value
        }

        $record | Add-Member -MemberType NoteProperty -Name $name -Value $value -Force
    }

    foreach($formattedValue in $Entity.FormattedValues.GetEnumerator()) {
        $formattedName = "$($formattedValue.Key)_FormattedValue"

        $record | Add-Member `
            -MemberType NoteProperty `
            -Name $formattedName `
            -Value $formattedValue.Value `
            -Force
    }

    return $record
}

function parseRecordsPage {
    PARAM( 
        [parameter(Mandatory=$true)]
        [object]$records,
        [parameter(Mandatory=$true)]
        [string] $logicalname,
        [parameter(Mandatory=$true)]
        [xml] $xml
    )
    $recordslist = New-Object 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
    foreach($record in $records.Values){   
        $null = $record.Add("original",$record)
        $null = $record.Add("logicalname",$logicalname)
        if($record.ContainsKey("ReturnProperty_Id "))
        {
            $null = $record.Add("ReturnProperty_Id",$record.'ReturnProperty_Id ')
            $null = $record.Remove("ReturnProperty_Id ")
        }
        #add entityReferences values as values
        ForEach($attribute in $record.Keys|Select)
        {
            if(-not $attribute.EndsWith("_Property")) { continue }
            
            #if aliased value BUT if it's an EntityRef... then ignore it
            if($record[$attribute].Value -is [Microsoft.Xrm.Sdk.AliasedValue])
            {
                if($record[$attribute].Value.Value -isnot [Microsoft.Xrm.Sdk.EntityReference])
                {
                    $attName = $attribute.Replace("_Property","")
                    $record[$attName] = $record[$attribute].Value.Value
                }
            }

            if($record[$attribute].Value -is [Microsoft.Xrm.Sdk.EntityReference])
            {
                $attName = $attribute.Replace("_Property","")
                $record[$attName] = $record[$attribute].Value.Name
            }
        }
      
        $hashtable = $record -as [Hashtable]

        #adding Dynamic EntityReference
        if ($hashtable.ReturnProperty_Id -and $hashtable.ReturnProperty_EntityName) {
            $hashtable.EntityReference = New-CrmEntityReference -EntityLogicalName $hashtable.ReturnProperty_EntityName -Id $hashtable.ReturnProperty_Id
        }

        $recordslist.Add([pscustomobject]$hashtable)
    }
    $recordslist
}

function Coalesce {
    foreach($i in $args){
        if($i -ne $null){
            return $i
        }
    }
}

function VerifyCrmConnectionParam {
    [CmdletBinding()]
    PARAM( 
        [parameter(Mandatory=$false)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$conn,
        [parameter(Mandatory=$false)]
        [bool]$pipelineValue
    )
    #we have a $conn value and we were not given a $conn value so we should try to find one
    if($conn -eq $null -and $pipelineValue -eq $false)
    {
        $connobj = Get-Variable conn -Scope global -ErrorAction SilentlyContinue
        if($connobj.Value -eq $null)
        {
            Write-Error "A connection to Dataverse is required, use Get-Dataverse to connect."
            throw 'A connection to Dataverse is required, use Get-Dataverse to connect.'
        }
        else
        {
            $conn = $connobj.Value
        }
    }elseif($conn -eq $null -and $pipelineValue -eq $true){
        Write-Error "Connection object provided is null"
        throw "Connection object provided is null"
    }
    return $conn
}

function MapFieldTypeByFieldValue {
    PARAM(
        [Parameter(Mandatory=$true)][AllowNull()]
        [object]$Value
    )

    $valueTypeToSdkTypeMapping = @{
        "Boolean"         = [System.Boolean]
        "DateTime"        = [System.DateTime]
        "Decimal"         = [System.Decimal]
        "Single"          = [System.Single]
        "Money"           = [Microsoft.Xrm.Sdk.Money]
        "Int32"           = [System.Int32]
        "EntityReference" = [Microsoft.Xrm.Sdk.EntityReference]
        "OptionSetValue"  = [Microsoft.Xrm.Sdk.OptionSetValue]
        "String"          = [System.String]
        "Guid"            = [System.Guid]
    }

    # default is RAW
    $crmDataType = $null

    if($Value -ne $null) {

        $valueType = $Value.GetType().Name
        
        if($valueTypeToCrmTypeMapping.ContainsKey($valueType)) {
            $crmDataType = $valueTypeToCrmTypeMapping[$valueType]
        }   
    }

    return $crmDatatype
}

function GuessPrimaryKeyField() {
    PARAM(
        [Parameter(Mandatory=$true)]
        [object]$EntityLogicalName
    )

    $standardActivityEntities = @(
        "opportunityclose",
        "socialactivity",
        "campaignresponse",
        "letter","orderclose",
        "appointment",
        "recurringappointmentmaster",
        "fax",
        "email",
        "activitypointer",
        "incidentresolution",
        "bulkoperation",
        "quoteclose",
        "task",
        "campaignactivity",
        "serviceappointment",
        "phonecall"
    )
    # Some Entity has different pattern for id name.
    if($EntityLogicalName -eq "usersettings")
    {
        $primaryKeyField = "systemuserid"
    }
    elseif($EntityLogicalName -eq "systemform")
    {
        $primaryKeyField = "formid"
    }
    elseif($EntityLogicalName -in $standardActivityEntities)
    {
        $primaryKeyField = "activityid"
    }
    else 
    {
        # default
        $primaryKeyField = $EntityLogicalName + "id"
    }
    
    $primaryKeyField
}

function AddTls12Support {
    #by default PowerShell will show Ssl3, Tls - since SSL3 is not desirable we will drop it and use Tls + Tls12
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls -bor [System.Net.SecurityProtocolType]::Tls12
}

function ApplyCrmServiceClientObjectTemplate {
    [CmdletBinding()]
    PARAM( 
        [parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$conn
    )
    try{
        $defaultPropsCrmServiceClient = @(
            'IsReady',
            'IsBatchOperationsAvailable',
            'MaxRetryCount',
            'RetryPauseTime', 
            'Authority',
            'ActiveAuthenticationType',
            'OAuthUserId',
            'TenantId',
            'EnvironmentId',
            'ConnectedOrgId',
            'CrmConnectOrgUriActual',
            'ConnectedOrgFriendlyName',
            'ConnectedOrgUniqueName',
            'ConnectedOrgVersion',
            'SdkVersionProperty',
            'CallerId',
            'CallerAADObjectId',
            'DisableCrossThreadSafeties',
            'SessionTrackingId',
            'ForceServerMetadataCacheConsistency', 
            'LastCrmError'
        )
        $defaultPropsSetCrmServiceClient=New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultPropsCrmServiceClient)
        $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultPropsSetCrmServiceClient)
        $conn| Add-Member MemberSet PSStandardMembers $PSStandardMembers -Force
    }Catch{
        Write-Verbose "Failed to set a new PSStandardMember on connection object"
    }
}
## Taken from CRM SDK sample code
## https://msdn.microsoft.com/en-us/library/microsoft.crm.sdk.messages.retrieveentityribbonresponse.compressedentityxml.aspx
function UnzipCrmRibbon {
    PARAM( 
        [parameter(Mandatory=$true)]
        [Byte[]]$Data
    )

    $memStream = New-Object System.IO.MemoryStream

    $memStream.Write($Data, 0, $Data.Length)
    $package = [System.IO.Packaging.ZipPackage]::Open($memStream, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
    $part = $package.GetPart([System.Uri]::new('/RibbonXml.xml', [System.UriKind]::Relative))

    try
    {
        $strm = $part.GetStream()
        $reader = [System.Xml.XmlReader]::Create($strm)

        $xmlDoc = New-Object System.Xml.XmlDocument
        $xmlDoc.Load($reader)

        return $xmlDoc
    }
    finally
    {
        if ($strm -ne $null)
        {
            $strm.Dispose()
            $strm = $null
        }
        if ($reader -ne $null)
        {
            $reader.Dispose()
            $reader = $null
        }
    }
}