$policyData */ public function extractForPolicyData(InventoryItem $item, array $policyData): array { $warnings = []; $edges = collect(); $edges = $edges->merge($this->extractAssignedTo($item, $policyData, $warnings)); $edges = $edges->merge($this->extractScopedBy($item, $policyData)); // Enforce max 50 outbound edges by priority: assigned_to > scoped_by > others $priorities = [ RelationshipType::AssignedTo->value => 1, 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 extractAssignedTo(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; } // Known shapes: ['target' => ['groupId' => '...']] or direct ['groupId' => '...'] $groupId = Arr::get($assignment, 'target.groupId') ?? Arr::get($assignment, 'groupId'); if (is_string($groupId) && $groupId !== '') { $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::AssignedTo->value, 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'aad_group', ], ]; } else { $warning = [ 'type' => 'unsupported_reference', 'policy_id' => (string) ($policyData['id'] ?? $item->external_id), '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); } }