Salvar, listar, editar e excluir favoritos de busca — API
Permite ao usuário salvar um conjunto de filtros da worklist com um nome ("favorito de busca"), listar os próprios favoritos, consultar um específico, renomear ou substituir seus filtros, e excluí-lo. É um recurso inteiramente privado ao usuário autenticado — não existe, hoje, compartilhamento entre usuários (ver nota na visão geral).
Funcionamento
- Valida o token e a permissão
EXAM_READ, sempre com escopocurrent-user(listar, criar) ouresource/rls(encontrar, editar, excluir um favorito específico por id). - Valida o corpo (
ValidationPipede classe:whitelist+forbidNonWhitelisted+transform, além doValidationPipeglobal). - Para criar: verifica se já existe, para o mesmo usuário, um favorito com o mesmo nome
(comparação sem diferenciar maiúsculas/minúsculas, ignorando favoritos já excluídos); se
houver, recusa com
409. Os filtros são validados e normalizados porDiagnosisSavedWorklistFilter(mesmo modelo usado pela busda padrão). - Para editar: busca o favorito pelo id e pelo dono (
findActiveByIdAndUser); se não achar (não existe, foi excluído, ou é de outro usuário), responde404— nunca403, para não revelar a um usuário que o id pertence a outra pessoa. Um corpo vazio (nemnamenemfilters) é rejeitado; enviarfilters: nullexplicitamente também é rejeitado (não existe hoje a operação de "limpar filtros" — para isso, edite os filtros para um objeto vazio{}, nãonull). Renomear verifica conflito de nome de novo, ignorando o próprio registro. - Para excluir: confirma posse do mesmo jeito, depois faz soft delete (
deleted_at) — a validação de conflito de grupo mencionada no Swagger (409 LISTING_PREFERENCE_ASSOCIATION_CONFLICT) ainda não está implementada (ver visão geral): hoje a exclusão é sempre incondicional. - Toda criação, edição e exclusão grava um evento de auditoria com o estado anterior/novo dos filtros.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| GET | /v1/worklist-preferences/listings | Lista os favoritos de busca do usuário autenticado |
| GET | /v1/worklist-preferences/listings/:id | Busca um favorito específico do usuário |
| POST | /v1/worklist-preferences/listings | Cria um favorito de busca |
| PATCH | /v1/worklist-preferences/listings/:id | Renomeia e/ou substitui os filtros de um favorito |
| DELETE | /v1/worklist-preferences/listings/:id | Exclui (soft delete) um favorito |
Versão: v1
Swagger: GET /worklist-preferences/listings ·
Rota (Dev): http://localhost:3000/v1/worklist-preferences/listings
Lógica de decisão de POST/PATCH (conflito de nome e validação → desfechos):
Permissões
| Rota | Guards | Permissão | Escopo |
|---|---|---|---|
GET /listings, POST /listings | JwtAuthenticationGuard, AuthorizationGuard | EXAM_READ (any) | current-user |
GET/PATCH/DELETE /listings/:id | idem | EXAM_READ (any) | resource (worklist-listing-preference), enforcement rls, identificador = :id |
O escopo resource/rls garante que o registro consultado/alterado pertence ao ator autenticado;
um id válido de outro usuário responde 404, não 403.
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim | Bearer <access_token> |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | uuid | Sim (exceto GET/POST da coleção) | Id do favorito de busca |
Query parameters
Nenhum.
Body
POST /listings — CreateDiagnosisListingPreferenceRequest:
json{"name": "Urgência e emergência","filters": { "priorities": ["URGENT", "EMERGENCY"] }}
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
name | string | Sim | @IsNotEmpty @MaxLength(255) |
filters | objeto | Sim | ver mapa de campos abaixo (DiagnosisSavedWorklistFilterRequest) |
PATCH /listings/:id — UpdateDiagnosisListingPreferenceRequest:
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
name | string | Não | @IsNotEmpty @MaxLength(120) — atenção: o limite de tamanho aqui é 120, mais baixo que o limite de 255 aceito na criação e no modelo persistido |
filters | objeto ou null | Não | se presente, mesmo formato da criação; null explícito é rejeitado (400); omitir mantém os filtros atuais |
Corpo precisa ter pelo menos um dos dois campos; um corpo {} é rejeitado com 400 INVALID_WORKLIST_PREFERENCE.
Mapa de campos de filters (DiagnosisSavedWorklistFilterRequest, usado por favoritos e por
busca padrão):
| Campo | Tipo | Valores possíveis | Observação |
|---|---|---|---|
unitIds, radiologistIds, teamUserIds, specialtyIds | string[] | uuids | deduplicados e ordenados ao salvar |
term, patientName, patientCode, patientIdentity, requestingPhysicianName, accessionNumber | string | ≤120 caracteres | |
studyDescription | string | ≤255 caracteres | |
insurance | string | ≤50 caracteres | |
interestingReason | string | ≤30 caracteres | sem fonte de dados projetada — ver busca avançada |
statuses | enum[] (ExamStatus) | — | |
priorities | enum[] (ExamPriority) | — | |
modalities | enum[] (ExamModality) | — | |
sources | enum[] (ExamSource) | — | |
aiLevels | enum[] (WorklistAiLevel) | ABSENT, LOW, MEDIUM, HIGH | sem fonte de dados projetada |
includeUnassignedRadiologist | boolean | — | |
isCriticalFinding, isTechnicalSuspicion, isDuplicate, isOriginal, isReleased, isComplement, isPrior | boolean | — | |
priorMode | enum (WorklistPriorMode) | ONLY | |
hasAttachments, hasComments, hasKeyImages, hasAttachmentKeyImages, hasPatientAttachments, hasExamAttachments, hasAudio, slaExpired | boolean | — | |
performedAtFrom/To, createdAtFrom/To, transferredAtFrom/To | date (ISO) | — | início não pode ser depois do fim |
examId | uuid | — | |
patientSex | enum (ExamPatientSex) | — | |
patientBirthDate | string YYYY-MM-DD | — | |
interval | enum (WorklistInterval) | inclui ALL | ALL é normalizado para THIRTY_DAYS ao salvar — ver RN-02 |
dateMode | enum (WorklistDateMode) | TRANSFERRED, PREPARED |
Um campo fora dessa lista (PERSISTABLE_FILTER_KEYS) é rejeitado com 400 INVALID_WORKLIST_PREFERENCE — diferente da busca avançada, que aceita campos extras e apenas os
ignora (ver busca avançada).
Response
200 — GET /listings ({ data: DiagnosisListingPreferenceResponse[] }):
json{"data": [{"id": "c4e2....","userId": "8f2a....","name": "Urgência e emergência","filters": { "priorities": ["EMERGENCY", "URGENT"] },"shared": false,"readOnly": false,"createdAt": "2026-09-01T12:00:00.000Z","updatedAt": "2026-09-01T12:00:00.000Z"}]}
shared e readOnly são hoje sempre false — reservados para o compartilhamento por grupo ainda
não implementado (ver visão geral).
201 — POST /listings: mesmo formato de um item de data acima.
200 — PATCH /listings/:id: idem, refletindo o novo name/filters.
204 — DELETE /listings/:id: sem corpo.
Erros
| Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
| (guard) | UNAUTHENTICATED | 401 | token ausente, inválido ou expirado |
| (guard) | FORBIDDEN_ACTION | 403 | falta EXAM_READ |
| (validação) | BAD_REQUEST | 400 | corpo malformado, campo fora do whitelist |
DiagnosisWorklistPreferenceInvalidError | INVALID_WORKLIST_PREFERENCE | 400 | nome fora de 1–255 chars, filters com campo não suportado, intervalo de data invertido, corpo de PATCH vazio, ou filters: null explícito |
DiagnosisListingPreferenceNotFoundError | LISTING_PREFERENCE_NOT_FOUND | 404 | id não existe, foi excluído, ou pertence a outro usuário |
DiagnosisListingPreferenceNameConflictError | LISTING_PREFERENCE_NAME_CONFLICT | 409 | já existe favorito do mesmo usuário com esse nome (case-insensitive) |
O Swagger também documenta um
409 LISTING_PREFERENCE_ASSOCIATION_CONFLICTparaDELETE(vínculo de grupo impeditivo), mas o código atual sempre exclui incondicionalmente — ver visão geral. Não confirmado que esse erro seja hoje alcançável.
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Nome é único por usuário, sem diferenciar maiúsculas/minúsculas | "Urgente" e "urgente" conflitam entre si para o mesmo usuário; favoritos de outro usuário não contam |
| RN-02 | interval: ALL nunca é persistido como está | salvo como THIRTY_DAYS — o favorito nunca guarda "sem limite" como intervalo relativo |
| RN-03 | PATCH distingue omitir de limpar | omitir filters mantém o snapshot atual; enviar filters: null é erro; renomear sem enviar filters não toca nos filtros salvos |
| RN-04 | Exclusão é sempre soft delete incondicional hoje | não há checagem de vínculo de grupo ativa (BLOQUEADO USR-09 no código) |
| RN-05 | Favorito nunca é compartilhado | shared/readOnly sempre false; não existe hoje um jeito de outro usuário enxergar este favorito |
| RN-06 | Filtros salvos aceitam mais critérios do que a busca avançada aplica hoje | aiLevels, interestingReason, priorMode, dateMode, specialtyIds, includeUnassignedRadiologist e os contadores derivados podem ser salvos num favorito, mas — se o favorito for aplicado chamando GET /exams com esses mesmos parâmetros — a busca ignora vários deles (ver busca avançada) |
Compliance
Não se aplica diretamente — o próprio favorito não expõe dado clínico ou de paciente além dos
critérios de busca que o usuário mesmo escolheu salvar (ex.: um nome de paciente digitado em
patientName). A leitura de exames que o favorito filtra segue as regras de
busca avançada.
Variáveis de ambiente
Nenhuma encontrada para esta rota.
Tempo médio de resposta
A confirmar — responsável: time de Diagnosis; data: 24/09/2026. Não há medição publicada.
Requisitos não funcionais
| Requisito | Definição |
|---|---|
| Idempotência | PUT não existe aqui; PATCH não é idempotente por natureza mas repetir a mesma chamada produz o mesmo estado final; DELETE é idempotente na prática (segunda chamada responde 404, não erro de estado) |
| Paginação | Não — GET /listings devolve a lista completa do usuário |
| Rate limit | Limite global padrão da API (ver visão geral) |
| Cache | Não |
| Auditoria | Sim — diagnosis.worklist.listing-preference-created/updated/deleted, com snapshot antes/depois dos filtros |
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026.(destino de front:search-favorites,search-favorite-details,search-favorite-edit) - 📂 Módulo: Busca
- 🔎 Relacionado: Busca avançada de exames, Busca padrão