Skip to main content

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 por examId nã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 DiagnosisListingPreferenceResponse sempre devolve shared: false e readOnly: false, e o MaintainDiagnosisWorklistPreferencesService.deleteListingPreference traz 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 campos shared/readOnly e o Swagger já documentar um erro 409 LISTING_PREFERENCE_ASSOCIATION_CONFLICT para 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áginaCobre
Busca avançada de examesGET /exams, GET /exams/metadata/counts, GET /exams/exports
Favoritos de buscaGET/POST /worklist-preferences/listings, GET/PATCH/DELETE /worklist-preferences/listings/:id
Busca padrãoGET/PUT/DELETE /worklist-preferences/default-search

Fora do escopo desta documentação. DiagnosisWorklistSharedDefaultsController expõe rotas de administração (/tenants/:tenantId/worklist-defaults, /units/:unitId/worklist-defaults) que permitem a um administrador (permissão POLICY_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. Ver package/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). :::