modules/Azure/Discovery/Private/InvokeCIEMGraphEdgeBuild.ps1

function InvokeCIEMGraphEdgeBuild {
    <#
    .SYNOPSIS
        Transforms collected relationship objects into graph_edges rows.
    .DESCRIPTION
        Maps relationship names (member_of, owner_of, has_role_member, transitive_member_of) to
        PascalCase edge kinds and saves them as non-computed (computed=0) graph edges.
        Skips edges where source or target nodes do not exist in graph_nodes (FK constraint safety).
        Skips unknown relationship types with a warning log entry.
    .PARAMETER Relationships
        Array of relationship objects with SourceId, TargetId, Relationship, SourceType, TargetType.
    .PARAMETER Connection
        SQLite connection for transaction support. Pass $null for standalone operations.
    .PARAMETER CollectedAt
        ISO 8601 timestamp for the collection time.
    .OUTPUTS
        [int] Total number of edges created.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$Relationships,

        [Parameter(Mandatory)]
        [AllowNull()]
        [object]$Connection,

        [Parameter(Mandatory)]
        [string]$CollectedAt
    )

    $ErrorActionPreference = 'Stop'

    # Map relationship names to edge kinds (PascalCase)
    $kindMap = @{
        'member_of'            = 'MemberOf'
        'owner_of'             = 'OwnerOf'
        'has_role_member'      = 'HasRoleMember'
        'transitive_member_of' = 'TransitiveMemberOf'
    }

    $edgeCount = 0

    # Node existence cache to avoid repeated DB queries (N+1 prevention)
    $nodeExistsCache = @{}

    # Helper: check if node exists in graph, with caching
    # Must use same $Connection to see uncommitted nodes within a transaction
    function NodeExists([string]$nodeId) {
        if (-not $nodeId) { return $false }
        if ($nodeExistsCache.ContainsKey($nodeId)) { return $nodeExistsCache[$nodeId] }
        $fkParams = @{ Query = "SELECT 1 FROM graph_nodes WHERE id = @id"; Parameters = @{ id = $nodeId } }
        if ($Connection) { $fkParams.Connection = $Connection }
        $exists = (@(Invoke-CIEMQuery @fkParams).Count -gt 0)
        $nodeExistsCache[$nodeId] = $exists
        $exists
    }

    foreach ($rel in $Relationships) {
        $edgeKind = $kindMap[$rel.Relationship]
        if (-not $edgeKind) {
            Write-CIEMLog "Unknown relationship type '$($rel.Relationship)', skipping" -Severity WARNING -Component 'GraphBuilder'
            continue
        }

        # Only create edge if both source and target nodes exist (FK constraint)
        if (-not (NodeExists $rel.SourceId) -or -not (NodeExists $rel.TargetId)) { continue }

        $splat = @{
            SourceId    = $rel.SourceId
            TargetId    = $rel.TargetId
            Kind        = $edgeKind
            Computed    = 0
            CollectedAt = $CollectedAt
        }
        if ($Connection) { $splat.Connection = $Connection }

        Save-CIEMGraphEdge @splat
        $edgeCount++
    }

    Write-CIEMLog "Graph edge build: $edgeCount collected edges created" -Component 'GraphBuilder'
    $edgeCount
}