Skip to main content

Pesquisar, contar e exportar exames — API

Pesquisa a worklist de exames com dezenas de filtros combináveis (texto, período, status, modalidade, médico, flags clínicas), devolve a contagem de exames por categoria para as abas/badges da tela, e exporta o resultado em CSV. As três rotas compartilham a mesma autorização, o mesmo modelo de filtro e o mesmo mascaramento de dados de paciente.

Funcionamento​

  1. Valida o token (JwtAuthenticationGuard) e a permissão EXAM_READ (AuthorizationGuard, escopo unit-list/policy — ver visão geral).
  2. Valida a query (ValidationPipe global: whitelist + forbidNonWhitelisted + transform) — qualquer campo não reconhecido ou de tipo errado é 400.
  3. Monta o filtro de busca a partir da query (ver observação sobre campos sem efeito abaixo) e reconstrói a lista de unidades pesquisáveis cruzando unitIds pedidos com as unidades ativas do ator.
  4. Se, depois desse cruzamento, não sobrar nenhuma unidade, devolve página vazia (total: 0) sem consultar o banco.
  5. Aplica os filtros na worklist: texto (ILIKE parcial, sem acentuação/normalização especial), listas de enum (IN), período de datas, o filtro de "anteriores" (política própria, ver RN-06), a janela relativa (interval), e a expansão de especialidade em subespecialidades.
  6. Se aiLevels ou interestingReason chegarem até o filtro (isso só acontece em GET /exams/metadata/counts, não em GET /exams — ver observação), a busca é recusada com 503: esses dois critérios ainda não têm fonte de dados projetada.
  7. Pagina e ordena o resultado; mascara os dados de paciente conforme a permissão do ator na unidade do exame; grava um evento de auditoria (diagnosis.exam.search, sem PHI, só metadados).
  8. Para exportação, os mesmos filtros de GET /exams valem, mas com uma faixa de data obrigatória de no máximo 31 dias, e o resultado sai como texto CSV, não como um arquivo.

Divergência confirmada no código. DiagnosisWorklistController.filterOf() (diagnosis-worklist.controller.ts:238-280) monta o filtro de GET /exams (e, por reuso, de GET /exams/exports) copiando campo a campo da query — e não copia includeUnassignedRadiologist, specialtyIds, priorMode, dateMode, aiLevels, interestingReason, hasAttachmentKeyImages, hasPatientAttachments nem hasExamAttachments. Esses campos são validados normalmente pelo DTO (nenhum erro é retornado) mas não têm efeito nenhum no resultado de GET /exams nem de GET /exams/exports hoje. Em contraste, GET /exams/metadata/counts monta o filtro espalhando a query inteira ({ ...query, hasAudio: ... }, linha 151-157) e por isso aplica todos esses campos normalmente — inclusive disparando o 503 de aiLevels/interestingReason quando presentes. O mesmo trecho referencia query.attachmentScope e query.hasCapturedImages (linhas 244 e 247), que não existem em SearchDiagnosisWorklistQuery — esses nomes pertencem a um sistema de busca de exames diferente (packages/exam); não foi possível confirmar neste levantamento se isso é apenas um resíduo inofensivo (sempre undefined) ou sinal de um build quebrado nesta branch de integração. Vale reportar ao time de Diagnosis antes de assumir que a busca avançada aceita todos os filtros que o favorito/busca padrão aceitam.

Endpoints​

MétodoRotaDescrição
GET/v1/examsPesquisa a worklist de exames, paginada e ordenada
GET/v1/exams/metadata/countsConta exames por prioridade, status e "atribuídos a mim"
GET/v1/exams/exportsExporta o resultado da busca em CSV (máx. 31 dias, 20.000 linhas)

Versão: v1

Swagger: GET /exams · GET /exams/metadata/counts · Rota (Dev): http://localhost:3000/v1/exams

O endpoint de exportação (GET /exams/exports) não tem @ApiOperation no controller — ele aparece no Swagger sob a tag "Diagnosis — Worklist", mas sem summary/operationId próprios.

Lógica de decisão da rota de busca (validações, escopo de unidade e critério indisponível → desfechos):

Permissões​

RotaGuardsPermissãoEscopo
GET /examsJwtAuthenticationGuard, AuthorizationGuardEXAM_READ (any)unit-list, filtro policy
GET /exams/metadata/countsidemEXAM_READ (any)unit-list, filtro policy
GET /exams/exportsidemEXAM_EXPORT_WORKLIST (any)unit-list, filtro policy

Sem a permissão exigida, a resposta é 403 FORBIDDEN_ACTION. O escopo por unidade não é aplicado pelo guard — ver visão geral.

Headers​

HeaderObrigatórioDescrição
AuthorizationSimBearer <access_token>

Path parameters​

Nenhum.

Query parameters​

Parâmetros de GET /exams e GET /exams/exports (SearchDiagnosisWorklistQuery). GET /exams/metadata/counts aceita os mesmos campos, mais interval e includeAudio (tabela seguinte).

NomeTipoValores possíveisDefaultAplicado em /exams?
pageint1–10.0001Sim
limitint1–10020Sim
sortByenumcreatedAt, performedAt, slaExpirationDate, transferredAtcreatedAtSim
sortOrderenumASC, DESCDESCSim
termstring (≤120)busca livre em nome, código e descrição do estudo—Sim
patientName, studyDescription, patientCode, patientIdentity, requestingPhysicianName, accessionNumber, insurancestringsubstring (ILIKE)—Sim
unitIdsstring[]ids de unidadetodas as ativas do atorSim (cruzado com o claim)
statusesenum[] (ExamStatus)status do exame—Sim
prioritiesenum[] (ExamPriority)——Sim
modalitiesenum[] (ExamModality)——Sim
sourcesenum[] (ExamSource)——Sim
radiologistIdsstring[]ids de médico—Sim
includeUnassignedRadiologistbooleaninclui exames sem médico atribuído (OR com radiologistIds)—Não
teamUserIdsstring[]médico OU participante—Sim
specialtyIdsstring[]expandido em subespecialidades ativas—Não
isCriticalFinding, isTechnicalSuspicion, isDuplicate, isOriginal, isReleased, isComplementboolean——Sim
isPriorbooleanver política de anteriores (RN-06)—Sim
priorModeenumONLY (força só anteriores)—Não
hasAttachments, hasComments, hasKeyImages, hasAudiobooleancontador > 0 / = 0—Sim
hasAttachmentKeyImagesbooleanimagem-chave, contando também as do paciente (contador legado)—Não
hasPatientAttachmentsbooleansó anexos do paciente—Não
hasExamAttachmentsbooleanattachment_count + captured_image_count—Não
slaExpiredbooleanSLA vencido (sla_expiration_date <= now())—Sim
includeDeletedbooleaninclui exames com deleted_at preenchidofalseSim
performedAtFrom/To, createdAtFrom/To, transferredAtFrom/Todate (ISO)intervalo fechado; início não pode ser depois do fim—Sim
examIduuidfiltra por um exame específico — não ignora os demais filtros (diferente da busca legada)—Sim
patientSexenum (ExamPatientSex)——Sim
patientBirthDatestring YYYY-MM-DD——Sim
dateModeenumTRANSFERRED, PREPARED — troca a coluna de data usada pela janela relativa e pelo período de realização—Não
aiLevelsenum[] (WorklistAiLevel)ABSENT, LOW, MEDIUM, HIGH — sem fonte de dados projetada—Não (mas dispara 503 em /metadata/counts)
interestingReasonstring (≤30)sem fonte de dados projetada—Não (idem)

Somente em GET /exams/metadata/counts:

NomeTipoValores possíveisDefault
intervalenum (WorklistInterval)TODAY, 2_DAYS, 7_DAYS, 30_DAYS, 60_DAYS, 90_DAYS, 6_MONTHS, LAST_YEAR, ALL—
includeAudioenum (WorklistAudioFilter)WITH_AUDIO, WITHOUT_AUDIO, ALL — sobrepõe hasAudio quando informado—

Body​

Nenhum — as três rotas são GET.

Response​

200 — GET /exams (DiagnosisWorklistEnvelopeResponse):

json
{
"data": [
{
"examId": "8f2a1c3e-....",
"unitId": "3b6d....",
"patientName": "***",
"patientIdentity": "***34",
"patientBirthDate": null,
"status": "SIGNED",
"priority": "URGENT",
"modality": "CT",
"studyDescription": "Tórax",
"slaExpirationDate": "2026-09-25T12:00:00.000Z",
"transferredAt": "2026-09-24T09:00:00.000Z"
}
],
"meta": { "page": 1, "limit": 20, "total": 42, "totalPages": 3 }
}

O item completo tem muitos outros campos (contadores de anexo/imagem/áudio, dados de médico solicitante/executante etc.); essa forma de exibição do exame é definida pelo módulo Exames — aqui documentamos apenas os campos relevantes para busca/mascaramento. patientName/patientEmail saem mascarados (***) e patientIdentity mascarado com os 2 últimos caracteres visíveis quando o ator não tem PATIENT_READ_IDENTITY na unidade do exame.

200 — GET /exams/metadata/counts:

json
{
"data": {
"total": 128,
"buckets": [
{ "category": "URGENT", "count": 12 },
{ "category": "SIGNED", "count": 40 },
{ "category": "ASSIGNED_TO_ME", "count": 7 }
]
}
}

Os buckets sempre incluem uma linha por valor de ExamPriority e uma por valor de ExamStatus (mesmo com contagem 0), mais ASSIGNED_TO_ME (exames em que o ator é o médico responsável ou consta em participant_ids).

200 — GET /exams/exports:

json
{ "data": { "csv": "exam_id,unit_id,patient_name,...\n...\n", "rows": 318, "fileName": "worklist-2026-09-24.csv" } }

O CSV tem 13 colunas fixas (exam_id, unit_id, patient_name, patient_code, patient_identity, accession_number, modality, study_description, status, priority, radiologist_name, performed_at, sla_expiration_date) — não é configurável pelo cliente. Valores que começam com =, +, -, @, tab ou retorno de carro ganham um ' na frente (proteção contra injeção de fórmula ao abrir em planilha).

Erros​

Classe de erroerrorCodeStatusQuando ocorre
(guard)UNAUTHENTICATED401token ausente, inválido ou expirado
(guard)FORBIDDEN_ACTION403falta EXAM_READ (busca/contagem) ou EXAM_EXPORT_WORKLIST (exportação)
(validação de query)BAD_REQUEST400campo não reconhecido, tipo inválido, page/limit fora da faixa
DiagnosisWorklistPreferenceInvalidErrorINVALID_WORKLIST_PREFERENCE400filtro reconstruído é inconsistente (ex.: intervalo de data com início depois do fim) — checagem redundante à do DTO, na reconstrução do DiagnosisWorklistFilter
DiagnosisWorklistCriterionUnavailableErrorWORKLIST_CRITERION_UNAVAILABLE503aiLevels ou interestingReason chegaram ao filtro (hoje, só possível via /metadata/counts — ver observação acima)
DiagnosisWorklistProjectionFailedErrorWORKLIST_PROJECTION_FAILED503a agregação de /metadata/counts devolveu uma linha inválida (defesa interna, não deveria ocorrer em operação normal)
ValidationErrorDIAGNOSIS_WORKLIST_EXPORT_RANGE_REQUIRED400exportação sem performedAtFrom/To nem createdAtFrom/To completos
ValidationErrorDIAGNOSIS_WORKLIST_EXPORT_RANGE_INVALID400exportação com faixa de data invertida ou maior que 31 dias

Regras de negócio​

IDRegraComportamento esperado
RN-01term busca em três colunas ao mesmo tempopatient_name, patient_code e study_description, todas com ILIKE '%valor%', unidas por OR
RN-02Escopo por unidade cruza pedido × claimver visão geral; interseção vazia → página vazia sem erro
RN-03examId não é exclusivoao contrário da busca legada (Elastic), informar examId não ignora os demais filtros — o exame só aparece se também bater com eles
RN-04"Sem médico" é uma condição própriaincludeUnassignedRadiologist=true sem radiologistIds filtra só exames sem médico atribuído; nunca cai para "sem filtro"
RN-05Especialidade sem subespecialidade ativa não é ignoradase specialtyIds não mapear nenhuma subespecialidade ativa, o resultado é vazio (1 = 0), nunca a lista completa
RN-06Política de "anteriores" tem quatro desfechospriorMode=ONLY força is_prior=true; isPrior explícito vale como está; sem nenhum dos dois, o status pesquisado decide (sem status → só não-anteriores; inclui SIGNED → não-anteriores ou assinados; outros status → sem filtro de anterior)
RN-07Janela relativa (interval) respeita a coluna de data do dateModepor padrão mira transferred_at; com dateMode=PREPARED, mira prepared_at; uma janela de transferência explícita (transferredAtFrom/To) prevalece sobre o interval só quando ambos mirariam a mesma coluna padrão
RN-08aiLevels/interestingReason bloqueiam a busca em vez de ignorar o critériopor especificação, um filtro sem fonte projetada nunca é silenciosamente ignorado — a API responde 503, não devolve resultado parcial
RN-09Exportação exige janela de data curtaperformedAtFrom/To ou, na ausência, createdAtFrom/To, completos e com no máximo 31 dias entre si
RN-10Exportação lê em páginas de 500 e para em 20.000 linhasacima do limite, o CSV sai truncado (wasTruncated vai só para a auditoria, não para a resposta)
RN-11Mascaramento de PII é decidido por unidade, não globalmenteum mesmo ator pode ver o paciente revelado em uma unidade e mascarado em outra, conforme claim.unitMaskOf(unitId)

Compliance​

Órgão / normaExigênciaComo a rota atende
LGPDminimização e controle de acesso a dado pessoalnome, identidade, nascimento e e-mail do paciente só saem completos com PATIENT_READ_IDENTITY na unidade do exame; caso contrário, mascarados ou null
HIPAAtrilha de acesso a PHItoda busca (mesmo sem resultado) grava diagnosis.exam.search; toda exportação grava diagnosis.worklist.exported com contagem de linhas e se houve truncamento
ANVISA (indireto)resultado auditável e não silenciosamente incompletocritério sem fonte de dados (aiLevels/interestingReason) bloqueia a chamada em vez de devolver resultado parcial sem avisar (RN-08)

Variáveis de ambiente​

Nenhuma variável de ambiente própria deste módulo foi encontrada. Os limites de paginação (page ≤ 10.000, limit ≤ 100), o tamanho máximo de exportação (20.000 linhas, páginas de 500) e a janela máxima de exportação (31 dias) 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ênciaSim para GET /exams/GET /exams/metadata/counts (leitura); GET /exams/exports também é leitura, mas cada chamada reprocessa a busca (custo repetido)
PaginaçãoGET /exams: page/limit, máx. 100 itens por página, máx. página 10.000. GET /exams/exports pagina internamente (500 por vez) até 20.000 linhas
Rate limitSim — limite global padrão da API (100 req./60s, RateLimitGuard via APP_GUARD); nenhum limite específico deste módulo
CacheNão
AuditoriaSim — diagnosis.exam.search (busca e contagem) e diagnosis.worklist.exported (exportação), sem PHI nos metadados

Relacionado​

  • 🖥️ Tela: A confirmar — responsável: time de frontend; data: 24/09/2026. (esta rodada de documentação cobriu apenas o backend; ver destino de front exam-advanced-filter)
  • 📂 Módulo: Busca
  • ⭐ Relacionado: Favoritos de busca e Busca padrão — os filtros salvos usam o mesmo modelo de filtro desta página