Skip to main content

Anexar, listar, reclassificar e excluir anexos do exame — API

Gerencia os arquivos anexados a um exame — pedido médico, exame anterior, laudo externo, resultado, documento do paciente, termo de consentimento e outros. Cobre upload, listagem, consulta individual, reclassificação e exclusão. Não existe uma rota de download dedicada: cada anexo retornado carrega um link pré-assinado e temporário para baixar o arquivo diretamente do armazenamento de objetos.

Funcionamento​

Adicionar anexo (POST /exams/:examId/attachments): recebe um único arquivo (multipart/form-data) e uma classificação obrigatória. Resolve a unidade do exame, envia o arquivo ao armazenamento de objetos (bucket por tenant/unidade, subpasta exam-attachments) e só então grava a linha no banco. Se a gravação falhar depois do upload, o objeto recém-enviado é removido do armazenamento (limpeza de órfão) e o erro original é repropagado. Ao final, gera um evento de auditoria e agenda a reprojeção do exame na worklist.

Listar anexos (GET /exams/:examId/attachments): devolve todos os anexos ativos (não excluídos) do exame, ordenados primeiro pela ordem de exibição da classificação e, dentro da mesma classificação, pela data de criação. Cada item já vem com um link de download recém-gerado.

Buscar um anexo (GET /exams/:examId/attachments/:attachmentId): igual à listagem, mas para um único anexo; confirma que o anexo pertence ao examId informado antes de devolver.

Reclassificar anexo (PATCH /exams/:examId/attachments/:attachmentId/classification): troca a classificação de um anexo existente. Não altera o arquivo, apenas o metadado; gera evento de auditoria com o valor anterior e o novo.

Excluir anexo (DELETE /exams/:examId/attachments/:attachmentId): aplica soft-delete (marca deletedAt, apaga fileName/filePath do registro) e, em seguida, remove o objeto do armazenamento. Antes de excluir, verifica o escudo de exame assinado: se o exame está com status SIGNED ou RESIGNED, só é permitido excluir anexos classificados como EXAM_RESULTS; qualquer outra classificação é recusada. Se a remoção do objeto no armazenamento falhar depois do soft-delete já ter sido commitado, o erro é apenas registrado em log — a operação já é considerada bem-sucedida para quem chamou.

Endpoints​

MétodoRotaDescrição
POST/v1/exams/:examId/attachmentsAdiciona um anexo ao exame
GET/v1/exams/:examId/attachmentsLista os anexos ativos do exame
GET/v1/exams/:examId/attachments/:attachmentIdBusca um anexo específico do exame
PATCH/v1/exams/:examId/attachments/:attachmentId/classificationReclassifica um anexo
DELETE/v1/exams/:examId/attachments/:attachmentIdExclui (soft-delete) um anexo

Versão: v1

Swagger:

Rota (Dev): http://localhost:3000/v1/exams/:examId/attachments

Lógica de decisão de POST /exams/:examId/attachments (adicionar anexo):

Lógica de decisão de DELETE /exams/:examId/attachments/:attachmentId (excluir anexo):

Permissões​

Todas as rotas exigem sessão autenticada (JwtAuthenticationGuard) e são avaliadas pelo AuthorizationGuard num escopo RLS (resource: 'exam') resolvido pelo examId do path — ou seja, a permissão é checada sobre aquele exame específico, não de forma global.

RotaGuardsPermissão exigida
POST /exams/:examId/attachmentsJwtAuthenticationGuard, AuthorizationGuardexam:add-attachment
GET /exams/:examId/attachmentsJwtAuthenticationGuard, AuthorizationGuardexam:list-attachment
GET /exams/:examId/attachments/:attachmentIdJwtAuthenticationGuard, AuthorizationGuardexam:list-attachment
PATCH /exams/:examId/attachments/:attachmentId/classificationJwtAuthenticationGuard, AuthorizationGuardexam:add-attachment
DELETE /exams/:examId/attachments/:attachmentIdJwtAuthenticationGuard, AuthorizationGuardexam:delete-attachment

Sem token válido: 401 UNAUTHENTICATED. Com token válido mas sem a permissão no escopo do exame: 403 FORBIDDEN_ACTION.

Headers​

HeaderObrigatórioDescrição
AuthorizationSimBearer <access_token>
Content-TypeSim em POSTmultipart/form-data

Path parameters​

NomeTipoObrigatórioDescrição
examIdUUIDSimIdentificador do exame (ParseUUIDPipe)
attachmentIdUUIDSim (exceto no POST)Identificador do anexo (ParseUUIDPipe)

Query parameters​

Nenhum.

Body​

POST /exams/:examId/attachments — multipart/form-data:

CampoTipoObrigatórioValidação
filearquivo (binário)SimÚnico arquivo (FileInterceptor); limite de 50MB imposto pelo Multer; bytes não vazios verificados de novo na camada de domínio
classificacaostring (enum)Sim@IsEnum(AttachmentClassification) — ver valores abaixo
isExamePacientebooleanNãoAceita true/false ou as strings "true"/"false"
isImagemChavebooleanNãoAceita true/false ou as strings "true"/"false"

Valores de AttachmentClassification: OTHERS, PRIOR_EXAM, MEDICAL_REQUEST, EXTERNAL_REPORT, EXAM_RESULTS, PATIENT_DOCUMENT, CONSENT_FORM, OTHER_ATTACHMENTS.

Os nomes de campo do formulário (classificacao, isExamePaciente, isImagemChave) estão em português no contrato desta rota; os valores do enum de classificação e o restante do payload das outras rotas estão em inglês. Isso é o contrato real do código, não uma inconsistência da documentação.

PATCH /exams/:examId/attachments/:attachmentId/classification — UpdateDiagnosisAttachmentClassificationRequest:

json
{ "classification": "PATIENT_DOCUMENT" }
CampoTipoObrigatórioValidação
classificationstring (enum)Sim@IsEnum(AttachmentClassification)

GET e DELETE não recebem corpo.

Response​

201/200 — DiagnosisExamAttachmentResponse (envelope { "data": ... } em POST/GET; lista em GET de coleção):

json
{
"data": {
"id": "0196cf9a-...-uuid",
"examId": "0196cf9a-...-uuid",
"unitId": "0196cf9a-...-uuid",
"classification": "MEDICAL_REQUEST",
"fileName": "pedido-medico.pdf",
"fileType": "application/pdf",
"link": "https://storage.example.com/temporary-resource",
"createdAt": "2026-09-24T12:00:00.000Z"
}
}
CampoTipoDescrição
idUUIDIdentificador do anexo
examIdUUIDExame ao qual pertence
unitIdUUIDUnidade do exame
classificationstring (enum)Ver valores de AttachmentClassification acima
fileNamestring | nullNome original do arquivo no upload
fileTypestring | nullMIME type informado no upload
linkstring (URL) | nullURL pré-assinada de download, válida por 900 segundos (15 min); null quando não há arquivo associado
createdAtdatetimeData de criação do anexo

PATCH (reclassificar) devolve 200 sem corpo. DELETE devolve 204 sem corpo.

A resposta não expõe origin (interno vs. via convite externo), isKeyImage nem isPatientExam, embora esses três campos sejam gravados no banco no momento do upload. Quem consome a API hoje não consegue ler esses três metadados de volta por esta rota — lacuna confirmada no código, não uma omissão da documentação.

Erros​

RotaClasse de erroerrorCodeStatusQuando ocorre
POST(guard)UNAUTHENTICATED401token ausente/inválido/expirado
qualquer(guard)FORBIDDEN_ACTION403sem a permissão exigida no escopo do exame
POSTBadRequestException (Nest)BAD_REQUEST400nenhum arquivo enviado no multipart
POSTDiagnosisAttachmentFileRequiredErrorATTACHMENT_FILE_REQUIRED400arquivo enviado com 0 bytes
POSTDiagnosisAttachmentInputInvalidErrorATTACHMENT_INPUT_INVALID400classificação fora do enum, arquivo maior que 50MB, MIME mal formado, ou isKeyImage/isPatientExam com tipo diferente de boolean
POSTDiagnosisUnitNotFoundErrorUNIT_NOT_FOUND404a unidade do exame não foi encontrada no diretório de unidades
POST/PATCH(ValidationPipe global)BAD_REQUEST400corpo com campo obrigatório ausente ou fora do enum, antes mesmo de chegar à camada de domínio
GET/PATCH/DELETEDiagnosisExamAttachmentNotFoundErrorEXAM_ATTACHMENT_NOT_FOUND404anexo inexistente, já excluído, ou pertence a outro exame
DELETEDiagnosisAttachmentSignedShieldErrorATTACHMENT_SIGNED_SHIELD400exame SIGNED/RESIGNED e anexo não é EXAM_RESULTS

O Swagger de POST /exams/:examId/attachments documenta um 403 cuja descrição menciona anexos vindos "do portal de prescrição" com anexo externo desabilitado (EXTERNAL_ATTACHMENTS_DISABLED). Isso não corresponde ao código desta rota: addAttachment nunca chama a verificação de política de anexo externo — essa checagem só existe no fluxo de convite de anexo externo. Divergência de documentação no controller, não comportamento real desta rota.

Regras de negócio​

IDRegraComportamento esperado
RN-01Upload é feito no armazenamento antes da gravação em bancose a gravação falhar depois, o objeto recém-enviado é removido (evita órfão)
RN-02Falha ao remover o objeto na exclusão não desfaz o soft-deleteo soft-delete já foi commitado; a falha de storage só é logada como aviso
RN-03Listagem e busca só retornam anexos ativossoft-delete (deletedAt) exclui o registro das consultas por padrão do TypeORM
RN-04Ordem de exibição segue a classificação, não a dataordem fixa: MEDICAL_REQUEST → EXTERNAL_REPORT → EXAM_RESULTS → PATIENT_DOCUMENT → PRIOR_EXAM → CONSENT_FORM → OTHER_ATTACHMENTS → OTHERS; dentro da mesma classificação, mais antigo primeiro
RN-05Escudo de exame assinado protege anexos de exclusãoexame SIGNED ou RESIGNED: só anexos EXAM_RESULTS podem ser excluídos; qualquer outra classificação é recusada
RN-06Reclassificar não altera o arquivotroca somente o metadado classification; gera evento de auditoria com valor anterior e novo
RN-07O link de download é gerado a cada leitura, não persistidotoda chamada a POST, GET (lista ou item) gera uma nova URL pré-assinada com validade de 900s
RN-08Extensão do arquivo é sanitizada antes de compor o caminho de armazenamentosó aceita [a-z0-9]{1,10} extraído do nome original; extensão fora desse padrão é descartada (arquivo salvo sem extensão no caminho)

Compliance​

Órgão / normaExigênciaComo a rota atende
LGPDminimização e retenção controladasoft-delete preserva o registro para trilha/retenção sem manter o arquivo físico; fileName/filePath são apagados do registro no soft-delete
HIPAAtrilha de quem anexou/reclassificou/excluiueventos de auditoria diagnosis.exam.attachment-added, diagnosis.exam.attachment-reclassified, diagnosis.exam.attachment-removed, com actorUserId e contexto de auditoria
ANVISA (indireto)proteção de documentos de exame já assinadoescudo de exame assinado (RN-05) impede alterar a evidência documental de um exame já finalizado, exceto o próprio resultado

Variáveis de ambiente​

Nenhuma variável de ambiente específica desta rota foi encontrada no código lido; os limites (50MB por arquivo, 900s de validade do link) são constantes no código, não configuráveis por variável de ambiente.

Tempo médio de resposta​

A confirmar — responsável: time de Diagnosis; data: 24/09/2026. Não há medição publicada; não foi executado neste levantamento.

Requisitos não funcionais​

RequisitoDefinição
IdempotênciaNão — cada POST cria um novo anexo; DELETE de um anexo já excluído retorna 404 (não é idempotente em efeito)
PaginaçãoNão — GET de coleção sempre retorna todos os anexos ativos do exame
Rate limitNenhum específico destas rotas; sujeitas apenas ao limite global padrão da API
CacheNão
AuditoriaSim — ver eventos listados em Compliance

Relacionado​

  • 🖥️ Tela: A confirmar — responsável: time de frontend; data: 24/09/2026. (fora do escopo deste levantamento, que cobriu apenas o backend)
  • 📂 Módulo: Anexos (Attachment)
  • 🔗 Fluxo relacionado: Convite de anexo externo — via de upload sem login no portal, para o mesmo exame