$policyData */ public function extractForPolicyData(InventoryItem $item, array $policyData): array { $warnings = []; $edges = collect(); $edges = $edges->merge($this->extractAssignments($item, $policyData, $warnings)); $edges = $edges->merge($this->extractScopedBy($item, $policyData)); $edges = $edges ->unique(fn (array $e) => implode('|', [ (string) ($e['tenant_id'] ?? ''), (string) ($e['source_type'] ?? ''), (string) ($e['source_id'] ?? ''), (string) ($e['target_type'] ?? ''), (string) ($e['target_id'] ?? ''), (string) ($e['relationship_type'] ?? ''), ])) ->values(); // Enforce max 50 outbound edges by priority: assigned_to > scoped_by > others $priorities = [ RelationshipType::AssignedToInclude->value => 1, RelationshipType::AssignedToExclude->value => 2, RelationshipType::UsesAssignmentFilter->value => 3, RelationshipType::ScopedBy->value => 2, RelationshipType::Targets->value => 3, RelationshipType::DependsOn->value => 4, ]; /** @var Collection $sorted */ $sorted = $edges->sortBy(fn ($e) => $priorities[$e['relationship_type']] ?? 99)->values(); $limited = $sorted->take(50); $now = now(); $payload = $limited->map(function (array $e) use ($now) { $metadata = $e['metadata'] ?? null; if (is_array($metadata)) { // Ensure portability across SQLite/Postgres when using upsert via query builder $e['metadata'] = json_encode($metadata); } return array_merge($e, [ 'created_at' => $now, 'updated_at' => $now, ]); })->all(); if (! empty($payload)) { InventoryLink::query()->upsert( $payload, ['tenant_id', 'source_type', 'source_id', 'target_type', 'target_id', 'relationship_type'], ['metadata', 'updated_at'] ); } return $warnings; } /** * @param array $policyData * @return Collection> */ private function extractAssignments(InventoryItem $item, array $policyData, array &$warnings): Collection { $assignments = Arr::get($policyData, 'assignments'); if (! is_array($assignments)) { return collect(); } $edges = []; foreach ($assignments as $assignment) { if (! is_array($assignment)) { continue; } $policyId = (string) ($policyData['id'] ?? $item->external_id); $target = Arr::get($assignment, 'target'); $odataType = is_array($target) ? (Arr::get($target, '@odata.type') ?? Arr::get($target, '@OData.Type')) : null; $odataType = is_string($odataType) ? strtolower($odataType) : null; $groupId = Arr::get($assignment, 'target.groupId') ?? Arr::get($assignment, 'groupId'); $groupId = is_string($groupId) ? trim($groupId) : null; $filterId = Arr::get($assignment, 'deviceAndAppManagementAssignmentFilterId') ?? Arr::get($assignment, 'assignmentFilterId') ?? Arr::get($assignment, 'target.deviceAndAppManagementAssignmentFilterId'); $filterId = is_string($filterId) ? trim($filterId) : null; $filterType = Arr::get($assignment, 'deviceAndAppManagementAssignmentFilterType') ?? Arr::get($assignment, 'assignmentFilterType') ?? Arr::get($assignment, 'target.deviceAndAppManagementAssignmentFilterType'); $filterType = is_string($filterType) ? strtolower(trim($filterType)) : null; $filterMode = in_array($filterType, ['include', 'exclude'], true) ? $filterType : null; if (is_string($filterId) && $filterId !== '') { $edges[] = [ 'tenant_id' => (int) $item->tenant_id, 'source_type' => 'inventory_item', 'source_id' => (string) $item->external_id, 'target_type' => 'foundation_object', 'target_id' => $filterId, 'relationship_type' => RelationshipType::UsesAssignmentFilter->value, 'metadata' => array_filter([ 'last_known_name' => null, 'foundation_type' => 'assignment_filter', 'filter_mode' => $filterMode, ], fn ($v) => $v !== null), ]; } if (is_string($groupId) && $groupId !== '') { $relationshipType = RelationshipType::AssignedToInclude->value; if (is_string($odataType) && str_contains($odataType, 'exclusion')) { $relationshipType = RelationshipType::AssignedToExclude->value; } $edges[] = [ 'tenant_id' => (int) $item->tenant_id, 'source_type' => 'inventory_item', 'source_id' => (string) $item->external_id, 'target_type' => 'foundation_object', 'target_id' => $groupId, 'relationship_type' => $relationshipType, 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'aad_group', ], ]; continue; } // Known non-group targets (e.g. allDevices/allLicensedUsers) are out-of-scope for edges. if (is_string($odataType) && (str_contains($odataType, 'alldevices') || str_contains($odataType, 'alllicensedusers') || str_contains($odataType, 'allusers'))) { continue; } $warning = [ 'type' => 'unsupported_reference', 'policy_id' => $policyId, 'raw_ref' => $assignment, 'reason' => 'unsupported_assignment_target_shape', ]; $warnings[] = $warning; Log::info('Unsupported reference shape encountered', $warning); } return collect($edges); } /** * @param array $policyData * @return Collection> */ private function extractScopedBy(InventoryItem $item, array $policyData): Collection { $scopeTags = Arr::get($policyData, 'roleScopeTagIds'); if (! is_array($scopeTags)) { return collect(); } $edges = []; foreach ($scopeTags as $tagId) { if (is_string($tagId) && $tagId !== '') { $edges[] = [ 'tenant_id' => (int) $item->tenant_id, 'source_type' => 'inventory_item', 'source_id' => (string) $item->external_id, 'target_type' => 'foundation_object', 'target_id' => $tagId, 'relationship_type' => RelationshipType::ScopedBy->value, 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'scope_tag', ], ]; } } return collect($edges); } }