[ 'id', 'document_id', 'title', 'category', 'topic', 'legislation', 'essential', 'published', 'source_reference', 'rights_status', 'description', 'document_type', 'document_number', 'document_date', 'source_url', 'language', 'version_label', 'document_group_id', 'applicable_from', 'applicable_until', 'territorial_scope', 'territory_id', 'source_confirmation', 'legal_status', 'last_reviewed_at', 'document_identity_key', 'duplicate_of_resource_id', 'metadata_version', 'created_by', 'last_reviewed_by', 'independent_review', ], 'documents' => [ 'id', 'status', 'data_classification', 'sha256', 'deleted_at', 'file_name', 'media_type', 'storage_key', 'size_bytes', 'version_no', 'issuer', 'issued_on', 'valid_until', 'uploaded_by', 'rights_status', 'original_source', 'malware_scan_status', 'malware_scanned_sha256', 'malware_engine', 'malware_engine_version', 'malware_signatures_at', 'malware_scanned_at', 'malware_failure_reason', 'malware_scan_attempts', ], 'portal_resource_links' => ['id', 'resource_id', 'target_type', 'target_key', 'relationship', 'applicability'], 'portal_resource_revisions' => ['id', 'resource_id', 'revision_no', 'change_type', 'snapshot'], 'document_malware_scans' => ['id', 'document_id', 'file_sha256', 'result', 'engine', 'engine_version', 'started_at'], ]; $missing = []; foreach ($required as $table => $columns) { if (! Schema::hasTable($table)) { $missing[] = "tabela {$table}"; continue; } foreach (array_diff($columns, Schema::getColumnListing($table)) as $column) { $missing[] = "{$table}.{$column}"; } } $requiredMigrations = [ '2026_10_05_120000_extend_portal_document_catalogue', '2026_10_05_130000_add_document_malware_scanning', ]; if (! Schema::hasTable('migrations')) { $missing[] = 'repositório de migrações'; } else { $ran = DB::table('migrations')->whereIn('migration', $requiredMigrations)->pluck('migration')->all(); foreach (array_diff($requiredMigrations, $ran) as $migration) { $missing[] = "migração {$migration}"; } } if ($missing !== []) { $this->error('Esquema da biblioteca incompatível. Não activar o catálogo; faltam elementos de esquema ou migrações.'); foreach ($missing as $item) { $this->line('- '.$item); } return self::FAILURE; } foreach ([ ['portal_resources', 'rights_status', 'unconfirmed'], ['documents', 'rights_status', 'unconfirmed'], ['documents', 'malware_scan_status', 'pending'], ] as [$table, $column, $expectedDefault]) { $default = DB::selectOne( 'select column_default from information_schema.columns where table_schema = current_schema() and table_name = ? and column_name = ?', [$table, $column], )?->column_default; if (! is_string($default) || ! str_contains(strtolower($default), $expectedDefault)) { $this->error("Valor predefinido inseguro ou ausente em {$table}.{$column}; preservar os registos e rever a migração."); return self::FAILURE; } } $invalidCleanRecords = DB::table('documents')->where('malware_scan_status', 'clean') ->where(function ($query): void { $query->whereNull('malware_scanned_sha256')->orWhereColumn('malware_scanned_sha256', '!=', 'sha256'); })->count(); if ($invalidCleanRecords > 0) { $this->error('Existem documentos marcados como limpos sem correspondência do hash analisado.'); return self::FAILURE; } $this->info('Esquema da biblioteca compatível; verificações de direitos e análise antimalware disponíveis.'); return self::SUCCESS; } catch (Throwable $exception) { Log::error('Document library schema compatibility check failed.', ['reference' => $reference, 'exception' => $exception]); $this->error('Não foi possível verificar a compatibilidade do esquema. Consulte o registo interno. Referência: '.$reference); return self::FAILURE; } } }