Public/Deploy-ReportProject.ps1

Function Deploy-ReportProject {
       <#
    .SYNOPSIS
        Imports Power BI Project format (PBIP) files into a Fabric workspace.
     
    .DESCRIPTION
        This function deploys Power BI Project (PBIP) files (either .pbir or .pbism) from a specified
        file system path into a Microsoft Fabric workspace. It can create new items or update existing ones.
         
        The function handles:
        - Power BI Report (.pbir) files
        - Semantic Model (.pbism) files
         
        It extracts metadata from the project files and builds appropriate API requests to deploy
        to the target Fabric workspace.
 
    .PARAMETER path
        The file system path containing the Power BI Project files to import.
        Must contain either .pbir or .pbism files.
 
    .PARAMETER workspaceId
        The ID of the target Fabric workspace where items will be deployed.
 
    .PARAMETER itemProperties
        Optional hashtable to override item properties like type, displayName, or semanticModelId.
        Example: @{"type" = "SemanticModel"; "displayName" = "My Model"; "semanticModelId" = "12345abc-..."}
 
    .PARAMETER skipIfExists
        If specified, the function will not update items that already exist in the target workspace.
 
    .EXAMPLE
        Deploy-ReportProject -path "C:\Projects\MyReport" -workspaceId "12345678-abcd-1234-efgh-123456789012"
         
        Deploys all PBIP files from the specified path to the target workspace.
 
    .EXAMPLE
        Deploy-ReportProject -path "C:\Projects\MyModel" -workspaceId "12345678-abcd-1234-efgh-123456789012" `
            -itemProperties @{"displayName"="Production Model"} -skipIfExists
         
        Deploys the model with a custom display name and skips updating if it already exists.
 
    .OUTPUTS
        Returns a hashtable with the deployed item's id, displayName and type.
 
    .NOTES
        For reports that reference semantic models by path, you must first deploy the semantic model
        and then provide its ID through the itemProperties.semanticModelId parameter.
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$path
        ,
        [Parameter(Mandatory)]
        [string]$workspaceId
        ,
        [hashtable]$itemProperties
        ,
        [switch]$skipIfExists
    )

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

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

    if ($itemsInFolder.Count -eq 0) {
        Write-Log "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-Log "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-Log "File override '.platform'"
                $itemMetadataStr = [System.Text.Encoding]::UTF8.GetString($fileOverrideMatch.Value)
            }
        }

        $itemMetadata = $itemMetadataStr | ConvertFrom-Json

        $itemType = $itemMetadata.metadata.type

        $displayName = $itemMetadata.metadata.displayName

        Write-Log "Item type: $itemMetadata"
    }

    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

            $datasetId = $itemProperties.semanticModelId

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

                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.semanticModelId' parameter."
                }
                else {
                    Write-Log "Report connected to semantic model: $datasetId"
                }

                if ($pbirJson.datasetReference.byPath)
                {
                    $pbirJson.datasetReference.byPath = $null
                }
                Write-Log "Report connected to semantic model: $datasetId"
                $byConnectionObj = @{
                    "connectionString"          = $null #"Data Source=powerbi://api.powerbi.com/v1.0/myorg/MojDev;Initial Catalog=test1;Access Mode=readonly;Integrated Security=ClaimsToken"
                    "pbiServiceModelId"         = $null
                    "pbiModelVirtualServerName" = "sobe_wowvirtualserver"
                    "pbiModelDatabaseName"      = $datasetId                
                    "name"                      = "EntityDataSource"
                    "connectionType"            = "pbiServiceXmlaStyleLive"
                }

                $pbirJson.datasetReference | Add-Member -NotePropertyName "byConnection" -NotePropertyValue $byConnectionObj -Force

                $newPBIR = $pbirJson | 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 = if ([System.Environment]::OSVersion.Platform -eq 'Unix') {
            $filePath.Replace($itemPathAbs, "").TrimStart("/").Replace("\", "/")
        } else {
            $filePath.Replace($itemPathAbs, "").TrimStart("\").Replace("\", "/")
        }

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

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

    Write-Log "Total parts: $($parts.Count)"

    $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-Log "Item '$displayName' of type '$itemType' already exists." -ForegroundColor Yellow

        $itemId = $foundItem.id
    }

    if ($itemId -eq $null) {
        Write-Log "Creating a new item"

        # Prepare the request

        $itemRequest = @{ 
            
            displayName = $displayName
            #type = $itemType
            definition  = @{
                Parts = $parts
                format      = "PBIR"
            }
        } | ConvertTo-Json -Depth 3        
        
        Write-Log "Item request: $($parts)"

        foreach ($x in $parts) {
            Write-Log "Part: $($x.Path)"
        }
        $createItemResult = Invoke-FabricAPIRequest -uri "workspaces/$workspaceId/reports"  -method Post -body $itemRequest


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

        $itemId = $createItemResult.id

        Write-Log "Created a new item with ID '$itemId'" -ForegroundColor Green

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

        if ($skipIfExists) {
            Write-Log "Item '$displayName' of type '$itemType' already exists. Skipping." -ForegroundColor Yellow
        }
        else {
            Write-Log "Updating item definition"

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

            Write-Log "Updated item with ID '$itemId'" -ForegroundColor Green
        }

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