.nuget/custom_modules/FabricPS-PBIP.psm1

$script:apiUrl = "https://api.fabric.microsoft.com/v1"
$script:resourceUrl = "https://api.fabric.microsoft.com" 
$script:fabricToken = $null

# Load TOM Assembly, required to manipulate the TMSL/TMDL of semantic models

$currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent)

$nugets = @(
    @{
        name    = "Microsoft.AnalysisServices.NetCore.retail.amd64"
        ;
        version = "19.84.1"
        ;
        path    = @(
            "lib\netcoreapp3.0\Microsoft.AnalysisServices.Core.dll"
            , "lib\netcoreapp3.0\Microsoft.AnalysisServices.Tabular.dll"
            , "lib\netcoreapp3.0\Microsoft.AnalysisServices.Tabular.Json.dll"
        )
    }
)

foreach ($nuget in $nugets) {
    if (!(Test-Path -path "$currentPath\.nuget\$($nuget.name).$($nuget.version)*" -PathType Container)) {
        
        Write-Host "Downloading and installing Nuget: $($nuget.name)"

        Install-Package -Name $nuget.name -ProviderName NuGet -Destination "$currentPath\.nuget" -RequiredVersion $nuget.Version -SkipDependencies -AllowPrereleaseVersions -Scope CurrentUser  -Force
    }
    
    foreach ($nugetPath in $nuget.path) {
        Write-Host "Loading assembly: '$nugetPath'"

        $path = Resolve-Path -LiteralPath (Join-Path "$currentPath\.nuget\$($nuget.name).$($nuget.Version)" $nugetPath)
        
        Add-Type -Path $path -Verbose | Out-Null
    }
   
}

function Get-FabricAuthToken {
    <#
    .SYNOPSIS
        Get the Fabric API authentication token
    #>

    [CmdletBinding()]
    param
    (
    )

    if (!$script:fabricToken) {                
        Set-FabricAuthToken
    }
    
    Write-Output $script:fabricToken
}

function Set-FabricAuthToken {
    <#
    .SYNOPSIS
        Set authentication token for the Fabric service
    #>

    [CmdletBinding()]
    param
    (
        [string]$servicePrincipalId        
        ,
        [string]$servicePrincipalSecret
        ,
        [PSCredential]$credential
        ,
        [string]$tenantId 
        ,
        [switch]$reset
        ,
        [string]$apiUrl
    )

    if (!$reset) {
        $azContext = Get-AzContext
    }
    
    if ($apiUrl) {
        $script:apiUrl = $apiUrl
    }

    if (!$azContext) {
        
        Write-Host "Getting authentication token"
        
        if ($servicePrincipalId) {
            $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $servicePrincipalId, ($servicePrincipalSecret | ConvertTo-SecureString -AsPlainText -Force)

            Connect-AzAccount -ServicePrincipal -TenantId $tenantId -Credential $credential | Out-Null

            Set-AzContext -Tenant $tenantId | Out-Null
        }
        elseif ($credential -ne $null) {
            Connect-AzAccount -Credential $credential -Tenant $tenantId | Out-Null
        }
        else {
            Connect-AzAccount | Out-Null
        }

        $azContext = Get-AzContext        
    }

    Write-Host "Connnected: $($azContext.Account)"

    $script:fabricToken = (Get-AzAccessToken -ResourceUrl $script:resourceUrl).Token
}

Function Invoke-FabricAPIRequest {
    <#
    .SYNOPSIS
        Sends an HTTP request to a Fabric API endpoint and retrieves the response.
        Takes care of: authentication, 429 throttling, Long-Running-Operation (LRO) response
    #>

    [CmdletBinding()]        
    param(                                    
        [Parameter(Mandatory = $false)] [string] $authToken,
        [Parameter(Mandatory = $true)] [string] $uri,
        [Parameter(Mandatory = $false)] [ValidateSet('Get', 'Post', 'Delete', 'Put', 'Patch')] [string] $method = "Get",
        [Parameter(Mandatory = $false)] $body,        
        [Parameter(Mandatory = $false)] [string] $contentType = "application/json; charset=utf-8",
        [Parameter(Mandatory = $false)] [int] $timeoutSec = 240,        
        [Parameter(Mandatory = $false)] [int] $retryCount = 0
    )

    if ([string]::IsNullOrEmpty($authToken)) {
        $authToken = Get-FabricAuthToken
    }    

    $fabricHeaders = @{
        'Content-Type'  = $contentType
        'Authorization' = "Bearer {0}" -f $authToken
    }

    try {
        
        $requestUrl = "$($script:apiUrl)/$uri"

        Write-Verbose "Calling $requestUrl"
        
        # If need to use -OutFile beware of the following breaking change: https://github.com/PowerShell/PowerShell/issues/20744

        # TODO: use -SkipHttpErrorCheck to read the entire error response, need to find a solution to handle 429 errors: https://stackoverflow.com/questions/75629606/powershell-webrequest-handle-response-code-and-exit

        $response = Invoke-WebRequest -Headers $fabricHeaders -Method $method -Uri $requestUrl -Body $body  -TimeoutSec $timeoutSec     

        $lroFailOrNoResultFlag = $false

        if ($response.StatusCode -eq 202) {
            do {                
                $asyncUrl = [string]$response.Headers.Location

                Write-Host "Waiting for request to complete. Sleeping..."

                Start-Sleep -Seconds 5

                $response = Invoke-WebRequest -Headers $fabricHeaders -Method Get -Uri $asyncUrl

                $lroStatusContent = $response.Content | ConvertFrom-Json

            }
            while ($lroStatusContent.status -ine "succeeded" -and $lroStatusContent.status -ine "failed")

            if ($lroStatusContent.status -ieq "succeeded") {
                # Only calls /result if there is a location header, otherwise 'OperationHasNoResult' error is thrown

                $resultUrl = [string]$response.Headers.Location

                if ($resultUrl) {
                    $response = Invoke-WebRequest -Headers $fabricHeaders -Method Get -Uri $resultUrl    
                }
                else {
                    $lroFailOrNoResultFlag = $true
                }
            }
            else {
                $lroFailOrNoResultFlag = $true
                
                if ($lroStatusContent.error) {
                    throw "LRO API Error: '$($lroStatusContent.error.errorCode)' - $($lroStatusContent.error.message)"
                }
            }
            
        }

        #if ($response.StatusCode -in @(200,201) -and $response.Content)
        if (!$lroFailOrNoResultFlag -and $response.Content) {            
            $contentBytes = $response.RawContentStream.ToArray()

            # Test for BOM

            if ($contentBytes[0] -eq 0xef -and $contentBytes[1] -eq 0xbb -and $contentBytes[2] -eq 0xbf) {
                $contentText = [System.Text.Encoding]::UTF8.GetString($contentBytes[3..$contentBytes.Length])                
            }
            else {
                $contentText = $response.Content
            }

            $jsonResult = $contentText | ConvertFrom-Json

            if ($jsonResult.value) {
                $jsonResult = $jsonResult.value
            }

            Write-Output $jsonResult -NoEnumerate
        }        
    }
    catch {
          
        $ex = $_.Exception

        $message = $null

        if ($ex.Response -ne $null) {

            $responseStatusCode = [int]$ex.Response.StatusCode

            if ($responseStatusCode -in @(429)) {
                if ($ex.Response.Headers.RetryAfter) {
                    $retryAfterSeconds = $ex.Response.Headers.RetryAfter.Delta.TotalSeconds + 5
                }

                if (!$retryAfterSeconds) {
                    $retryAfterSeconds = 60
                }

                Write-Host "Exceeded the amount of calls (TooManyRequests - 429), sleeping for $retryAfterSeconds seconds."

                Start-Sleep -Seconds $retryAfterSeconds

                $maxRetries = 3
                
                if ($retryCount -le $maxRetries) {
                    Invoke-FabricAPIRequest -authToken $authToken -uri $uri -method $method -body $body -contentType $contentType -timeoutSec $timeoutSec -retryCount ($retryCount + 1)
                }
                else {
                    throw "Exceeded the amount of retries ($maxRetries) after 429 error."
                }
            }
            else {                
                $apiErrorObj = $ex.Response.Headers | ? { $_.key -ieq "x-ms-public-api-error-code" } | Select -First 1

                if ($apiErrorObj) {
                    $apiError = $apiErrorObj.Value[0]
                    
                    if ($apiError -ieq "ItemHasProtectedLabel") {
                        Write-Warning "Item has a protected label."
                    }
                    else {
                        $message = "$($ex.Message); API error code: '$apiError'"

                        throw $message
                    }
                }                
            }
        }
        else {
            $message = "$($ex.Message)"
        }
                
        if ($message) {
            throw $message
        }
            
    }

}

Function New-FabricWorkspace {
    <#
    .SYNOPSIS
        Creates a new Fabric workspace.
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$name
        ,
        [switch]$skipErrorIfExists       
        ,
        [string]$capacityId 
    )

    $itemRequest = @{ 
        displayName = $name
        capacityId = $capacityId
    } | ConvertTo-Json

    try {        
        $createResult = Invoke-FabricAPIRequest -Uri "workspaces" -Method Post -Body $itemRequest

        Write-Host "Workspace created: '$name'"

        Write-Output $createResult.id
    }
    catch {
        $ex = $_.Exception

        if ($skipErrorIfExists) {
            if ($ex.Message -ilike "*409*") {
                Write-Host "Workspace '$name' already exists"

                $listWorkspaces = Invoke-FabricAPIRequest -Uri "workspaces" -Method Get

                $workspace = $listWorkspaces | ? { $_.displayName -ieq $name }

                if (!$workspace) {
                    throw "Cannot find workspace '$name'"
                }
                
                Write-Output $workspace.id
            }
            else {
                throw
            }
        }        
    }
    
}

Function Remove-FabricWorkspace {
    <#
    .SYNOPSIS
        Deletes a Fabric workspace.
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$workspaceId     
    )

    try {        

        Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId" -Method Delete
    }
    catch {
        throw
    }
}


Function Get-FabricWorkspace {
    <#
    .SYNOPSIS
        Get Fabric workspaces
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$workspaceName
    )
      
    $result = Invoke-FabricAPIRequest -Uri "workspaces" -Method Get

    if ($workspaceName) {
        $workspace = $result | ? { $_.displayName -ieq $workspaceName }

        if (!$workspace) {
            throw "Cannot find workspace '$workspaceName'"
        }

        Write-Output $workspace
    }
    else {
        Write-Output $result
    }
    
}

Function Set-FabricWorkspacePermissions {
    <#
    .SYNOPSIS
        Sets workspace role permissions
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$workspaceId
        ,
        [Parameter(Mandatory)]
        $permissions
    )

    try {        

        
        $existingRoles = Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/roleAssignments" -Method Get
        
        foreach ($permission in $permissions) {
            $matchRole = $existingRoles | ? { $_.principal.id -ieq $permission.principal.id } | select -First 1
            
            if (!$matchRole) {
                Write-Host "Adding role '$($permission.role)' to '$($permission.principal.id)'"

                $request = $permission | ConvertTo-Json

                Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/roleAssignments" -Method Post -Body $request                
            }
            else {
                # If role already exists for principal, check the role

                if ($permission.role -ine $matchRole.role) {
                    Write-Host "Updating principal '$($permission.principal.id)' role to '$($permission.role)'"

                    $request = @{"role" = $permission.role } | ConvertTo-Json

                    Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/roleAssignments/$($permission.principal.id)" -Method Patch -Body $request
                }
            }
        }        
    }
    catch {
        throw
    }
}

Function Export-FabricItems {
    <#
    .SYNOPSIS
        Exports items from a Fabric workspace to a specified local file system destination.
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$path
        ,
        [Parameter(Mandatory)]
        [string]$workspaceId  
        ,
        # Focus only on report and semantic model, there are items that are not exportable.
        [scriptblock]$filter = { $_.type -in @("report", "SemanticModel") }
    )    

    $items = Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/items" -Method Get

    if ($filter) {        
        $items = $items | Where-Object $filter
    }    

    Write-Host "Existing items: $($items.Count)"

    foreach ($item in $items) {
        
        try {
            $itemId = $item.id
            $itemName = $item.displayName
            $itemType = $item.type
            
            $itemNamePath = $itemName.Split([IO.Path]::GetInvalidFileNameChars()) -join '_'
            $itemOutputPath = "$path\$workspaceId\$($itemNamePath).$($itemType)"        
                
            Export-FabricItem -workspaceId $workspaceId -itemId $itemId -path $itemOutputPath
        }
        catch {
            $ex = $_.Exception

            Write-Warning "Error exporting item '$itemId' - '$($ex.ToString())'"
        } 

    }
}

Function Export-FabricItem {
    <#
    .SYNOPSIS
        Exports item from a Fabric workspace to a specified local file system destination.
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$workspaceId
        ,
        [Parameter(Mandatory)]
        [string]$itemId
        ,    
        [Parameter(Mandatory)]
        [string]$path = '.\pbipOutput'
        ,
        [string]$format        
    )    

    $itemOutputPath = $path   
    
    Write-Host "Getting definition of: $itemId"

    #POST https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/items/{itemId}/getDefinition

    $response = $null

    $getDefinitionUrl = "workspaces/$workspaceId/items/$itemId/getDefinition"

    if ($format) {
        $getDefinitionUrl += "?format=$format"
    }

    $response = Invoke-FabricAPIRequest -Uri $getDefinitionUrl -Method Post

    $partCount = $response.definition.parts.Count

    Write-Host "Parts: $partCount"
        
    if ($partCount -gt 0) {
        foreach ($part in $response.definition.parts) {
            Write-Host "Saving part: $($part.path)"
                
            $outputFilePath = "$itemOutputPath\$($part.path.Replace("/", "\"))"

            $parentFolderPath = Split-Path $outputFilePath -Parent

            New-Item -ItemType Directory -Path $parentFolderPath -ErrorAction SilentlyContinue | Out-Null

            $parentFolderPath = Resolve-Path -LiteralPath $parentFolderPath

            $bytes = [Convert]::FromBase64String($part.payload)

            Set-Content -LiteralPath $outputFilePath $bytes -AsByteStream
        }
    }
      
}

Function Import-FabricItems {
    <#
    .SYNOPSIS
        Imports items using the Power BI Project format (PBIP) into a Fabric workspace from a specified file system source.

    .PARAMETER fileOverrides
        This parameter let's you override a PBIP file without altering the local file.
    
    .PARAMETER itemProperties
        This parameter let's you override item properties like type, displayName.
        E.g. -itemProperties @{"<Item Folder Name>" = @{"type" = "SemanticModel"; "displayName"="<Name of the model>"}}
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$path = '.\pbipOutput'
        ,
        [Parameter(Mandatory)]
        [string]$workspaceId
        ,
        [string[]]$filter = $null
        ,
        [hashtable]$fileOverrides
        ,
        [hashtable]$itemProperties
    )

    # Search for folders with .pbir and .pbism in it

    $itemsInFolder = Get-ChildItem  -LiteralPath $path -recurse -include *.pbir, *.pbism

    if ($filter) {
        $itemsInFolder = $itemsInFolder | ? { 
            $pathFolder = $_.Directory.FullName
            $filter | ? { $pathFolder -ilike $_ }
        }
    }

    if ($itemsInFolder.Count -eq 0) {
        Write-Host "No items found in the path '$path' (*.pbir; *.pbism)"
        return
    }

    Write-Host "Items in the folder: $($itemsInFolder.Count)"

    # File Overrides processing, convert all to base64 - Its the final format of the parts for Fabric APIs

    $fileOverridesEncoded = @()
    
    if ($fileOverrides) {
        foreach ($fileOverride in $fileOverrides.GetEnumerator()) {
            $fileContent = $fileOverride.Value

            # convert to byte array

            if ($fileContent -is [string]) {
                
                # If its a valid path, read it as byte[]
                
                if (Test-Path -LiteralPath $fileContent) {
                    $fileContent = [System.IO.File]::ReadAllBytes($fileContent)                        
                }
                else {
                    $fileContent = [system.Text.Encoding]::UTF8.GetBytes($fileContent)
                }
            }
            elseif (!($fileContent -is [byte[]])) {
                throw "FileOverrides value type must be string or byte[]"
            }
            
            $fileOverridesEncoded += @{Name = $fileOverride.Name; Value = $fileContent }
        }
    }

    # Get existing items of the workspace

    $items = Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/items" -Method Get

    Write-Host "Existing items in the workspace: $($items.Count)"

    # Datasets first

    $itemsInFolder = $itemsInFolder | Select-Object  @{n = "Order"; e = { if ($_.Name -like "*.pbism") { 1 } else { 2 } } }, * | sort-object Order    

    $datasetReferences = @{}

    foreach ($itemInFolder in $itemsInFolder) {    
        
        # Get the parent folder

        $itemName = $itemInFolder.Directory.Name
        $itemPath = $itemInFolder.Directory.FullName

        write-host "Processing item: '$itemPath'"

        $files = Get-ChildItem -LiteralPath $itemPath -Recurse -Attributes !Directory

        # Remove files not required for the API: item.*.json; cache.abf; .pbi folder

        $files = $files | ? { $_.Name -notlike "item.*.json" -and $_.Name -notlike "*.abf" -and $_.Directory.Name -notlike ".pbi" }        


        # Prioritizes reading the displayName and type from itemProperties parameter
        $itemType = $null
        $displayName = $null

        if ($itemProperties -ne $null) {            
            $foundItemProperty = $itemProperties."$itemName"

            if ($foundItemProperty) {
                $itemType = $foundItemProperty.type
    
                $displayName = $foundItemProperty.displayName
            }            
        }
        
        # Try to read the item properties from the .platform file if not found in itemProperties

        if ((!$itemType -or !$displayName) -and (Test-Path -LiteralPath "$itemPath\.platform")) {            
            $itemMetadataStr = Get-Content -LiteralPath "$itemPath\.platform"

            $fileOverrideMatch = $null
            if ($fileOverridesEncoded) {
                $fileOverrideMatch = $fileOverridesEncoded | ? { "$itemPath\.platform" -ilike $_.Name } | select -First 1
                if ($fileOverrideMatch) {
                    Write-Host "File override '.platform'"
                    $itemMetadataStr = [System.Text.Encoding]::UTF8.GetString($fileOverrideMatch.Value)
                }
            }

            $itemMetadata = $itemMetadataStr | ConvertFrom-Json

            $itemType = $itemMetadata.metadata.type
    
            $displayName = $itemMetadata.metadata.displayName
        }

        if (!$itemType -or !$displayName) {
            throw "Cannot import item if any of the following properties is missing: itemType, displayName"
        }

        $itemPathAbs = Resolve-Path -LiteralPath $itemPath

        $parts = $files | % {
            
            $fileName = $_.Name
            $filePath = $_.FullName   
            
            $fileOverrideMatch = $null

            if ($fileOverridesEncoded) {
                $fileOverrideMatch = $fileOverridesEncoded | ? { $filePath -ilike $_.Name } | select -First 1            
            }

            if ($fileOverrideMatch) {

                Write-Host "File override '$fileName'"

                $fileContent = $fileOverrideMatch.Value              
            }
            else {                
                if ($filePath -like "*.pbir") {                  
    
                    $fileContentText = Get-Content -LiteralPath $filePath
                    $pbirJson = $fileContentText | ConvertFrom-Json

                    if ($pbirJson.datasetReference.byPath -and $pbirJson.datasetReference.byPath.path) {

                        # try to swap byPath to byConnection

                        $reportDatasetPath = (Resolve-path -LiteralPath (Join-Path $itemPath $pbirJson.datasetReference.byPath.path.Replace("/", "\"))).Path

                        $datasetReference = $datasetReferences[$reportDatasetPath]       
                        
                        if ($datasetReference) {
                            # $datasetName = $datasetReference.name
                            
                            $datasetId = $datasetReference.id
                            
                            $pbirJson.datasetReference.byPath = $null

                            $pbirJson.datasetReference.byConnection = @{
                                "connectionString"          = $null                
                                "pbiServiceModelId"         = $null
                                "pbiModelVirtualServerName" = "sobe_wowvirtualserver"
                                "pbiModelDatabaseName"      = "$datasetId"                
                                "name"                      = "EntityDataSource"
                                "connectionType"            = "pbiServiceXmlaStyleLive"
                            }
            
                            $newPBIR = $pbirJson | ConvertTo-Json

                            # $newPBIR = @{
                            # "version" = "1.0"
                            # "datasetReference" = @{
                            # "byConnection" = @{
                            # "connectionString" = $null
                            # "pbiServiceModelId" = $null
                            # "pbiModelVirtualServerName" = "sobe_wowvirtualserver"
                            # "pbiModelDatabaseName" = "$datasetId"
                            # "name" = "EntityDataSource"
                            # "connectionType" = "pbiServiceXmlaStyleLive"
                            # }
                            # }
                            # } | ConvertTo-Json
                            
                            $fileContent = [system.Text.Encoding]::UTF8.GetBytes($newPBIR)

                        }
                        else {
                            throw "Item API dont support byPath connection, switch the connection in the *.pbir file to 'byConnection'."
                        }
                    }
                    # if its byConnection then just send original
                    else {
                        $fileContent = [system.Text.Encoding]::UTF8.GetBytes($fileContentText)
                    }
                }
                else {
                    $fileContent = Get-Content -LiteralPath $filePath -AsByteStream -Raw                
                }
            }

            $partPath = $filePath.Replace($itemPathAbs, "").TrimStart("\").Replace("\", "/")

            $fileEncodedContent = ($fileContent) ? [Convert]::ToBase64String($fileContent) : ""
            
            Write-Output @{
                Path        = $partPath
                Payload     = $fileEncodedContent
                PayloadType = "InlineBase64"
            }
        }

        Write-Host "Payload parts:"        

        $parts | % { Write-Host "part: $($_.Path)" }

        $itemId = $null

        # Check if there is already an item with same displayName and type
        
        $foundItem = $items | ? { $_.type -ieq $itemType -and $_.displayName -ieq $displayName }

        if ($foundItem) {
            if ($foundItem.Count -gt 1) {
                throw "Found more than one item for displayName '$displayName'"
            }

            Write-Host "Item '$displayName' of type '$itemType' already exists." -ForegroundColor Yellow

            $itemId = $foundItem.id
        }

        if ($itemId -eq $null) {
            write-host "Creating a new item"

            # Prepare the request

            $itemRequest = @{ 
                displayName = $displayName
                type        = $itemType    
                definition  = @{
                    Parts = $parts
                }
            } | ConvertTo-Json -Depth 3        

            $createItemResult = Invoke-FabricAPIRequest -uri "workspaces/$workspaceId/items"  -method Post -body $itemRequest

            $itemId = $createItemResult.id

            write-host "Created a new item with ID '$itemId' $([datetime]::Now.ToString("s"))" -ForegroundColor Green

            Write-Output @{
                "id"          = $itemId
                "displayName" = $displayName
                "type"        = $itemType 
            }
        }
        else {
            write-host "Updating item definition"

            $itemRequest = @{ 
                definition = @{
                    Parts = $parts
                }            
            } | ConvertTo-Json -Depth 3        
            
            Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/items/$itemId/updateDefinition" -Method Post -Body $itemRequest

            write-host "Updated item with ID '$itemId' $([datetime]::Now.ToString("s"))" -ForegroundColor Green

            Write-Output @{
                "id"          = $itemId
                "displayName" = $displayName
                "type"        = $itemType 
            }
        }

        # Save dataset references to swap byPath to byConnection

        if ($itemType -ieq "semanticmodel") {
            $datasetReferences[$itemPath] = @{"id" = $itemId; "name" = $displayName }
        }
    }

}

Function Import-FabricItem {
    <#
    .SYNOPSIS
        Imports items using the Power BI Project format (PBIP) into a Fabric workspace from a specified file system source.
    
    .PARAMETER itemProperties
        This parameter let's you override item properties like type, displayName.
        E.g. -itemProperties @{"type" = "SemanticModel"; "displayName"="<Name of the model>"}
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$path = '.\pbipOutput'
        ,
        [Parameter(Mandatory)]
        [string]$workspaceId
        ,
        [hashtable]$itemProperties
    )

    # Search for folders with .pbir and .pbism in it

    $itemsInFolder = Get-ChildItem -LiteralPath $path | ? { @(".pbism", ".pbir") -contains $_.Extension }

    if ($itemsInFolder.Count -eq 0) {
        Write-Host "Cannot find valid item definitions (*.pbir; *.pbism) in the '$path'"
        return
    }    

    if ($itemsInFolder | ? { $_.Extension -ieq ".pbir" }) {
        $itemType = "Report"
    }
    elseif ($itemsInFolder | ? { $_.Extension -ieq ".pbism" }) {
        $itemType = "SemanticModel"
    }
    else {
        throw "Cannot determine the itemType."
    }
    
    # Get existing items of the workspace

    $items = Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/items" -Method Get

    Write-Host "Existing items in the workspace: $($items.Count)"

    $files = Get-ChildItem -LiteralPath $path -Recurse -Attributes !Directory

    # Remove files not required for the API: item.*.json; cache.abf; .pbi folder

    $files = $files | ? { $_.Name -notlike "item.*.json" -and $_.Name -notlike "*.abf" -and $_.Directory.Name -notlike ".pbi" }        

    # Prioritizes reading the displayName and type from itemProperties parameter
    $displayName = $null
    
    if ($itemProperties -ne $null) {            
        $displayName = $itemProperties.displayName         
    }

    # Try to read the item properties from the .platform file if not found in itemProperties

    if ((!$itemType -or !$displayName) -and (Test-Path -LiteralPath "$path\.platform")) {            
        $itemMetadataStr = Get-Content -LiteralPath "$path\.platform"

        $fileOverrideMatch = $null
        if ($fileOverridesEncoded) {
            $fileOverrideMatch = $fileOverridesEncoded | ? { "$path\.platform" -ilike $_.Name } | select -First 1
            if ($fileOverrideMatch) {
                Write-Host "File override '.platform'"
                $itemMetadataStr = [System.Text.Encoding]::UTF8.GetString($fileOverrideMatch.Value)
            }
        }

        $itemMetadata = $itemMetadataStr | ConvertFrom-Json

        $itemType = $itemMetadata.metadata.type

        $displayName = $itemMetadata.metadata.displayName
    }

    if (!$itemType -or !$displayName) {
        throw "Cannot import item if any of the following properties is missing: itemType, displayName"
    }

    $itemPathAbs = Resolve-Path -LiteralPath $path

    $parts = $files | % {

        $filePath = $_.FullName
        
        if ($filePath -like "*.pbir") {

            $fileContentText = Get-Content -LiteralPath $filePath
            $pbirJson = $fileContentText | ConvertFrom-Json

            if ($pbirJson.datasetReference.byPath -and $pbirJson.datasetReference.byPath.path) {

                $datasetId = $itemProperties.semanticModelId

                if (!$datasetId) {
                    throw "Cannot import directly a report using byPath connection. You must first resolve the semantic model id and pass it through the 'itemProperties' parameter."
                }

                $pbirJson.datasetReference.byPath = $null

                $pbirJson.datasetReference.byConnection = @{
                    "connectionString"          = $null                
                    "pbiServiceModelId"         = $null
                    "pbiModelVirtualServerName" = "sobe_wowvirtualserver"
                    "pbiModelDatabaseName"      = "$datasetId"                
                    "name"                      = "EntityDataSource"
                    "connectionType"            = "pbiServiceXmlaStyleLive"
                }

                $newPBIR = $pbirJson | ConvertTo-Json
                
                # $newPBIR = @{
                # "version" = "1.0"
                # "datasetReference" = @{
                # "byConnection" = @{
                # "connectionString" = $null
                # "pbiServiceModelId" = $null
                # "pbiModelVirtualServerName" = "sobe_wowvirtualserver"
                # "pbiModelDatabaseName" = "$datasetId"
                # "name" = "EntityDataSource"
                # "connectionType" = "pbiServiceXmlaStyleLive"
                # }
                # }
                # } | ConvertTo-Json
                
                $fileContent = [system.Text.Encoding]::UTF8.GetBytes($newPBIR)
            }
            # if its byConnection then just send original
            else {
                $fileContent = [system.Text.Encoding]::UTF8.GetBytes($fileContentText)
            }
        }
        else {
            $fileContent = Get-Content -LiteralPath $filePath -AsByteStream -Raw
        }
        
        $partPath = $filePath.Replace($itemPathAbs, "").TrimStart("\").Replace("\", "/")

        $fileEncodedContent = ($fileContent) ? [Convert]::ToBase64String($fileContent) : ""
        
        Write-Output @{
            Path        = $partPath
            Payload     = $fileEncodedContent
            PayloadType = "InlineBase64"
        }
    }

    Write-Host "Payload parts:"        

    $parts | % { Write-Host "part: $($_.Path)" }

    $itemId = $null

    # Check if there is already an item with same displayName and type
    
    $foundItem = $items | ? { $_.type -ieq $itemType -and $_.displayName -ieq $displayName }

    if ($foundItem) {
        if ($foundItem.Count -gt 1) {
            throw "Found more than one item for displayName '$displayName'"
        }

        Write-Host "Item '$displayName' of type '$itemType' already exists." -ForegroundColor Yellow

        $itemId = $foundItem.id
    }

    if ($itemId -eq $null) {
        write-host "Creating a new item"

        # Prepare the request

        $itemRequest = @{ 
            displayName = $displayName
            type        = $itemType    
            definition  = @{
                Parts = $parts
            }
        } | ConvertTo-Json -Depth 3        

        $createItemResult = Invoke-FabricAPIRequest -uri "workspaces/$workspaceId/items"  -method Post -body $itemRequest

        $itemId = $createItemResult.id

        write-host "Created a new item with ID '$itemId' $([datetime]::Now.ToString("s"))" -ForegroundColor Green

        Write-Output @{
            "id"          = $itemId
            "displayName" = $displayName
            "type"        = $itemType 
        }
    }
    else {
        write-host "Updating item definition"

        $itemRequest = @{ 
            definition = @{
                Parts = $parts
            }            
        } | ConvertTo-Json -Depth 3        
        
        Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/items/$itemId/updateDefinition" -Method Post -Body $itemRequest

        write-host "Updated item with ID '$itemId' $([datetime]::Now.ToString("s"))" -ForegroundColor Green

        Write-Output @{
            "id"          = $itemId
            "displayName" = $displayName
            "type"        = $itemType 
        }
    }
}

Function Remove-FabricItems {
    <#
    .SYNOPSIS
        Removes selected items from a Fabric workspace.
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$workspaceId = $null
        ,
        [string]$filter = $null 
    )

    $items = Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/items" -Method Get

    Write-Host "Existing items: $($items.Count)"

    if ($filter) {
        $items = $items | ? { $_.DisplayName -like $filter }
    }

    foreach ($item in $items) {
        $itemId = $item.id
        $itemName = $item.displayName

        Write-Host "Removing item '$itemName' ($itemId)"
        
        Invoke-FabricAPIRequest -Uri "workspaces/$workspaceId/items/$itemId" -Method Delete
    }
    
}

Function Set-SemanticModelParameters {
    <#
    .SYNOPSIS
        TODO
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$path = $null
        ,
        [Parameter(Mandatory)]
        [hashtable]$parameters = $null
        ,
        [switch]$failIfNotFound
    )

    $modelPath = "$path\definition"

    $isTMSL = $false

    if (!(Test-Path -LiteralPath $modelPath)) {
        $modelPath = "$path\model.bim"
        $isTMSL = $true
    }

    if (!(Test-Path -LiteralPath $modelPath)) {
        throw "Cannot find semantic model definition: '$modelPath'"
    }

    $compatibilityMode = [Microsoft.AnalysisServices.CompatibilityMode]::PowerBI

    if ($isTMSL) {
        $modelText = Get-Content -LiteralPath $modelPath
    
        $database = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::DeserializeDatabase($modelText, $null, $compatibilityMode)
    }
    else {
        $database = [Microsoft.AnalysisServices.Tabular.TmdlSerializer]::DeserializeDatabaseFromFolder($modelPath)
    }

    $database.CompatibilityMode = $compatibilityMode

    # Set expression parameters

    $changedFlag = $false

    $parameters.GetEnumerator() | ? {

        $parameterName = $_.Name
        $parameterValue = $_.Value

        $modelExpression = $database.Model.Expressions.Find($parameterName)

        if (!$modelExpression) {
            if ($failIfNotFound) {
                throw "Cannot find model expression '$parameterName'"
            }
            else {
                Write-Host "Cannot find model expression '$parameterName'"
            }
        }
        else {
            Write-Host "Changing model expression '$parameterName'"
            $modelExpression.Expression = $modelExpression.Expression -replace """?(.*)""? meta", """$parameterValue"" meta"
            $changedFlag = $true
        }
    }

    if ($changedFlag) {
        $serializeOptions = New-Object Microsoft.AnalysisServices.Tabular.SerializeOptions
        
        if ([string]::IsNullOrEmpty($database.Name)) {
            # If serialized without name an error is thrown later on deserialize. TODO: Review

            $database.Name = "Unknown"
        }

        if ($isTMSL) {
            $modelText = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::SerializeDatabase($database, $serializeOptions)

            $modelText | Out-File $modelPath -Force
        }
        else {
            [Microsoft.AnalysisServices.Tabular.TmdlSerializer]::SerializeDatabaseToFolder($database, $modelPath)
        }
    }
}