Módulo Busca — visão geral
O módulo de busca implementa a pesquisa avançada de exames da worklist de diagnóstico, a
contagem de exames por categoria (para os badges/abas da tela), a exportação em CSV e as
preferências pessoais de busca do usuário: filtros salvos ("favoritos de busca") e a busca
padrão que é aplicada automaticamente ao abrir a worklist. Ele vive no pacote diagnosis/worklist
do backend (NestJS).
Contexto. Este módulo é a reimplementação atual — em Postgres, com um read model materializado por projeção assíncrona — do que antes era resolvido consultando um índice Elasticsearch diretamente (ver
mm-pacs-portal-api/core/services/elasticService.js, referência de negócio F-S01/F-S03 no CRM). As regras de negócio antigas (BR-ANL-S*) não foram usadas como fonte desta documentação: o comportamento hoje é outro (por exemplo, buscar porexamIdnão ignora mais os demais filtros, ao contrário do que a regra antiga fazia). Cada afirmação abaixo foi confirmada diretamente no código atual, não na regra legada.
Prefixo de rotas e versionamento
Como no restante da API, o versionamento é nativo do NestJS por URI (VersioningType.URI, versão
padrão 1): toda rota deste módulo é servida sob /v1/... (ex.: /v1/exams,
/v1/worklist-preferences/listings). Em desenvolvimento local, o serviço sobe na porta 3000
(MAIN_API_PORT, default 3000) e expõe o Swagger em http://localhost:3000/docs.
Arquitetura
A projeção que popula o read model da worklist (a partir dos dados de exame) é responsabilidade do módulo Exames — não é detalhada aqui, que cobre apenas as capacidades de busca/filtro e as preferências pessoais de pesquisa.
Escopo por unidade, não por guard
Diferente de rotas que recebem tenantId/unitId no path e validam o escopo diretamente no
guard, a busca e a contagem usam scope: { kind: 'unit-list', filter: 'policy' }: o guard só
confirma que o ator tem a permissão EXAM_READ em alguma unidade; quem de fato restringe as
unidades pesquisadas é o serviço (SearchDiagnosisWorklistService.scopedUnitIdsOf), cruzando as
unidades pedidas na query (unitIds) com as unidades ativas do claim do ator. Administradores
de plataforma (claim.isPlatformAdmin) não sofrem essa interseção. Se o ator não tem nenhuma
unidade ativa, a busca devolve uma página vazia sem consultar o banco; se pediu unidades às quais
não tem acesso, elas são silenciosamente removidas da busca (não é um erro 403).
Mascaramento de dados do paciente
O item retornado por GET /exams passa por DiagnosisWorklistItemView/DiagnosisWorklistItemResponse
antes de sair da API: se o ator não tem a permissão PATIENT_READ_IDENTITY na unidade do exame
(via claim.unitMaskOf(unitId)), patientName e patientEmail voltam mascarados (***),
patientBirthDate volta null, e patientIdentity volta com os dois últimos caracteres visíveis
(***XX). Administradores de plataforma sempre veem os dados completos. Esse mascaramento vale
tanto para GET /exams quanto para o CSV de GET /exams/exports.
Preferências são sempre privadas ao usuário
Favoritos de busca (/worklist-preferences/listings) e a busca padrão
(/worklist-preferences/default-search) são recursos por usuário: o scope: { kind: 'current-user' }
e o scope: { kind: 'resource', resource: 'worklist-listing-preference', enforcement: 'rls' }
garantem que cada ator só lista, encontra, edita ou apaga os próprios registros — buscar o
favorito de outro usuário por id resulta em 404, não 403 (o recurso é tratado como inexistente
para quem não é o dono).
A confirmar — responsável: time de Diagnosis; data: 24/09/2026. O código de
DiagnosisListingPreferenceResponsesempre devolveshared: falseereadOnly: false, e oMaintainDiagnosisWorklistPreferencesService.deleteListingPreferencetraz um comentário explícito (BLOQUEADO (USR-09)) dizendo que o compartilhamento de favoritos entre usuários de um grupo ainda depende de um contrato de grupos que não existe neste worktree. Ou seja: hoje não há favorito compartilhado nem exclusão bloqueada por vínculo de grupo, apesar de a resposta da API já reservar os camposshared/readOnlye o Swagger já documentar um erro 409LISTING_PREFERENCE_ASSOCIATION_CONFLICTpara esse caso.
Convenção de erros
Toda exceção de negócio estende BaseError/DomainError/ValidationError e já carrega seu
statusCode e errorCode fixos no próprio construtor. O filtro global (AllExceptionsFilter)
apenas repassa esses valores. As páginas abaixo listam os erros confirmados por rota.
Rate limiting
Este módulo não declara nenhum guard de rate limit próprio (nenhum @UseGuards(RateLimitGuard)
nos controllers). O RateLimitGuard está registrado globalmente (APP_GUARD, via
RateLimitModule.forRoot), então o limite padrão da API se aplica a todas as rotas daqui: 100
requisições por 60000 ms por padrão, configurável por RATE_LIMIT_LIMIT /
RATE_LIMIT_TTL_MS / RATE_LIMIT_ENABLED. Não há um segundo balde por identidade (usuário/e-mail)
como o do módulo de autenticação.
Páginas deste módulo
| Página | Cobre |
|---|---|
| Busca avançada de exames | GET /exams, GET /exams/metadata/counts, GET /exams/exports |
| Favoritos de busca | GET/POST /worklist-preferences/listings, GET/PATCH/DELETE /worklist-preferences/listings/:id |
| Busca padrão | GET/PUT/DELETE /worklist-preferences/default-search |
Fora do escopo desta documentação.
DiagnosisWorklistSharedDefaultsControllerexpõe rotas de administração (/tenants/:tenantId/worklist-defaults,/units/:unitId/worklist-defaults) que permitem a um administrador (permissãoPOLICY_WRITE) configurar uma busca padrão compartilhada por Tenant ou Unidade, resolvida com a precedência pessoal → grupo → unidade → tenant → produto (DiagnosisWorklistDefaultSourceScope). É uma capacidade real do pacote, mas não é "busca" nem "favorito" pessoal — não há tela de front associada neste levantamento — por isso não ganhou página própria aqui. Verpackage/diagnosis/worklist/http/rest/controller/diagnosis-worklist-shared-defaults.controller.ts.
:::tip OpenAPI
A documentação interativa (schemas + "Try it out") está disponível em /docs no ambiente onde a
API está rodando (local: http://localhost:3000/docs).
:::