Documentação / Inventário de acesso ao banco — `apps/fullstack` e `apps/worker`

Inventário de acesso ao banco — `apps/fullstack` e `apps/worker`

Entrar

Inventário de acesso ao banco — apps/fullstack e apps/worker

Documento gerado a partir de busca estática no código (prisma.*, $queryRaw, $executeRaw, $transaction) e revisão manual dos arquivos maiores. Não substitui EXPLAIN ANALYZE nem métricas de produção.

Convenções

| Coluna / termo | Significado | |----------------|-------------| | R | Leitura (findUnique, findFirst, findMany, count, aggregate, groupBy, $queryRaw de leitura) | | W | Escrita (create, update, upsert, delete, *Many, $executeRaw de escrita, INSERT em $queryRaw) | | Peso | Heurística de custo relativo na aplicação (volume de linhas, agregações, scans com filtro amplo, raw SQL em messages) | | Cache | Cache-aside Redis de resultado de consulta (cacheAside no fullstack) ou cache pontual no worker; ver secção Cache |

Cliente Prisma: @pilot-status/database (prisma importado nos apps).

Escopo: código de produção sob apps/fullstack/src e apps/worker/src, excluindo **/*.test.ts e **/test/**. Testes de integração/unitários também usam Prisma para fixtures; não estão listados linha a linha (ver Apêndice).

Picos no pool (fullstack): em MessageService e PrismaAnalyticsRepository, leituras agregadas (vários count, findMany+count, groupBy, queries de analytics) executam em sequência por pedido, em vez de Promise.all em paralelo, para reduzir o número de ligações ao pool em simultâneo (com possível aumento ligeiro da latência do request).


Legenda de peso

| Peso | Critério (exemplos no repositório) | |------|-------------------------------------| | Alta | Várias contagens/agregações em messages; $queryRaw com SUM/AVG/buckets; listagem paginada + count com filtros amplos ou ILIKE; findMany em lote com cursor para reconciliação; groupBy em messageAttempt; admin com $queryRaw agregando por tenant; fluxo send-message com múltiplas transações por job | | Média | findMany paginado com where indexável; updateMany limitado por batch; alguns joins | | Baixa | findUnique / findFirst por id ou chave natural; create/update unitário; leituras pequenas em tabelas de configuração |


Cache

Fullstack — cacheAside (Redis, resultado de leitura)

Implementação: apps/fullstack/src/lib/cache.ts. Domínios e TTL padrão: apps/fullstack/src/shared/cache-domains.ts.

| Domínio (CacheDomain) | TTL padrão (s) | Staleness | Onde é usado (entrada HTTP) | Loader / dados | |-------------------------|----------------|-----------|-----------------------------|------------------| | dashboard_overview | 15 | near_real_time | app/api/dashboard/route.ts | Estatísticas via MessageService / analytics | | analytics_charts | 60 | aggregated | app/api/analytics/dashboard/route.ts, app/api/v1/analytics/dashboard/route.ts, app/api/analytics/ask-ai/route.ts | MessageService.getAnalyticsDashboard (agregações em messages) | | projects_list | 300 | static | app/api/projects/route.ts | Lista de projetos (Prisma) | | templates_list / templates_details | 300 | static | app/api/templates/route.ts, app/api/templates/[id]/route.ts, app/api/v1/templates/route.ts, app/api/v1/templates/[id]/route.ts | Templates / detalhe | | webhooks | 300 | static | app/api/webhooks/route.ts | WebhookService.listByTenant | | api_keys | 20 | near_real_time | app/api/api-keys/route.ts | Lista de API keys | | logs_messages | 10 | near_real_time | app/api/logs/route.ts | Logs de mensagens (PrismaAnalyticsRepository.getLogs ou caminho raw) | | logs_optins | 10 | near_real_time | app/api/logs/optins/route.ts | WhatsAppTransactionalOptInService.listOptIns | | subscription_status | 5 | critical | app/api/subscription/route.ts | Estado de subscrição | | optin_status | 5 | critical | app/api/v1/messages/opt-in/route.ts | Opt-in por API pública | | profile | 600 | static | app/api/profile/route.ts | Perfil do utilizador | | production_request | 15 | near_real_time | app/api/production-request/route.ts | Pedido de produção | | onboarding_assets | 600 | static | app/api/internal/onboarding/assets/route.ts | ensureOnboardingAssets | | auth_accounts | 300 | static | app/api/auth/list-accounts/route.ts | Contas ligadas | | admin_users / admin_templates / admin_production_requests / admin_feedback | 15 | near_real_time | app/api/admin/route.ts (consoante tab) | Listagens admin | | logs_messages (chave inclui id da mensagem) | 10 | near_real_time | app/api/v1/messages/[id]/route.ts | MessageService.getByIdentifier | | tenant_config (chave auto_recharge nas partes do cache) | 600 | static | app/api/auto-recharge/route.ts | prisma.autoRechargeSettings.findUnique + Stripe |

Invalidação automática após mutações Prisma (best-effort): apps/fullstack/src/lib/prisma-cache-invalidation.tsregisterPrismaMutationListener mapeia modelo → domínios Redis. Nota: resolveTenantIds pode executar leituras extra (tenant.findMany, user.findMany, webhook.findMany, template.findMany, etc.) para descobrir tenantId quando o where não traz tenant explícito — custo indireto além da mutação.

Invalidação explícita (invalidateTenantDomain) em rotas: por exemplo webhooks, api-keys, templates, projects, profile, internal/onboarding/*, internal/messages/cancel-scheduled, v1/messages/cancel, admin ao aprovar templates/pedidos.

Fullstack — Redis sem cache-aside de query Prisma

| Uso | Ficheiros / serviços | |-----|---------------------| | Dedup / lock ingestão Evolution | internal/webhook/dual-ingestion.ts, handlers de webhook | | Eventos configuráveis do webhook (JSON) | services/webhook.service.ts — chaves ps:webhook:events:* | | Alertas instância desligada | internal/webhook/handlers/connection.ts, etc. | | Dedup pdev | app/api/pdev/webhook/route.ts |

Worker

| Tipo | Detalhe | |------|---------| | Sem cacheAside | Não existe camada equivalente ao fullstack para cachear resultados Prisma | | Cache pontual de lista | list-desired-instance-names.ts: resultado de whatsAppInstance.findMany pode ser guardado em Redis (TTL WORKER_INSTANCE_NAMES_LIST_CACHE_TTL_MS) | | Redis operacional | Locks (send-message), dedup RabbitMQ (evolution-consumer.ts), contadores retry (sent-delivery-retry.ts, sent-delivery-check.ts), alertas — não substituem leituras Prisma de negócio |

Nota: mutações Prisma feitas só no worker não passam pelo listener de invalidação do Next (registado no fullstack). O cache Redis do fullstack pode ficar stale até TTL ou até uma mutação ocorrer no processo fullstack / invalidação manual.


Fullstack — rotas app/api (Prisma direto ou via serviço citado)

Rotas com prisma no ficheiro (resumo). Onde só há serviços, indicam-se as operações principais no serviço (ver secção Serviços).

| Rota / ficheiro | R/W resumido | Peso | Cache (cacheAside / invalidação) | |-----------------|-------------|------|--------------------------------------| | admin/route.ts | R: $queryRaw contagens por tenant; user.findMany; productionRequest.findMany; feedback.findMany; template.findUnique (duas tabs); W: productionRequest.findUnique; project.upsert; $transaction productionRequest.update + project.update; productionRequest.update | Alta (raw agregado + listas admin) | cacheAside admin_*; invalidateTenantDomain em ações | | api-keys/route.ts | Via cacheAside → serviço/repo API keys | Média | api_keys | | auth/list-accounts/route.ts | Via cacheAsideauth / accounts | Baixa | auth_accounts | | auth/unlink-account/route.ts | W: limpa cache keys Redis por prefixo; orquestra auth (Prisma em lib/auth.ts) | Média | Usa buildTenantDomainPrefix; invalidações em auth | | auto-recharge/route.ts | Via serviço + cacheAside | Baixa–média | cacheAside | | auto-recharge/setup/route.ts | W: auto-recharge.service (Prisma) | Baixa | — | | billing/history/route.ts | R: histórico billing (Prisma no handler) | Média | — | | checkout/route.ts | R/W: subscription.findFirst/create; packagePurchase.create | Média | — | | dashboard/route.ts | Via cacheAside + MessageService / analytics | Alta (agregações) | dashboard_overview | | feedback/route.ts | W: feedback create | Baixa | — | | internal/auth/oauth/complete/route.ts | W: fluxo onboarding / user | Média | — | | internal/auth/phone-number/verify-otp/route.ts | W: OTP / user | Média | — | | internal/messages/[id]/resend/route.ts | W: MessageService / fila | Média | — | | internal/messages/[id]/route.ts | R/W: mensagem interna | Média | — | | internal/messages/[id]/trace/route.ts | R: message, messageAttempt, messageTimelineEvent | Média | — | | internal/messages/send/route.ts | W: envio interno via MessageService | Média | — | | internal/onboarding/confirm-phone/route.ts | R/W: project, projectWhatsAppOptIn, user | Baixa | invalidateTenantDomain("profile") | | internal/onboarding/test-message/route.ts | R/W: user, whatsAppInstance, tenant | Baixa | invalidateTenantDomain("profile") | | internal/onboarding/verify-auth/route.ts | R: project, projectWhatsAppOptIn | Baixa | — | | internal/templates/[id]/test/route.ts | R: templates / versões | Baixa | — | | internal/templates/test-draft/route.ts | R/W: rascunho | Média | — | | internal/webhook/dual-ingestion.ts | R: evolutionIngestionEvent.findUnique; W: $executeRaw upsert ingestion; evolutionWebhookEvent.create; evolutionIngestionEvent.update | Média–alta (volume webhook) | Redis lock/dedup, não cache de SELECT | | internal/webhook/handlers/connection.ts | R/W: whatsAppInstance, tenant, user | Média | Redis alertas | | internal/webhook/handlers/messages-update.ts | W: message.update | Média | — | | internal/webhook/handlers/messages-upsert.ts | R/W: project, projectWhatsAppOptIn (várias), webhook, apiKey | Média–alta | — | | internal/webhook/persist-evolution-message-jids.ts | W: evolutionMessageJid.upsert | Baixa | — | | internal/webhook/utils.ts | R: templateVersion.findUnique; whatsAppInstance.findUnique; tenant.findUnique; user.findMany | Média (caminhos longos) | — | | labels/* | R/W: labels/contacts via PrismaLabelRepository / handlers | Média | — | | logs/route.ts | Via cacheAsidePrismaAnalyticsRepository.getLogs | Alta com filtro status | logs_messages | | pdev/webhook/route.ts | W: pdevWebhookProcessedEvent.createMany | Baixa | Redis dedup | | production-request/route.ts | Via cacheAside | Baixa | production_request | | profile/route.ts | Via cacheAside + updates | Baixa | profile | | projects/route.ts | Via cacheAside + mutations | Média | projects_list | | stripe/webhook/route.ts | R/W: user, autoRechargeSettings.upsert, tenant, packagePurchase, subscription (várias) | Média | — | | subscription/route.ts | Via cacheAside | Baixa | subscription_status | | templates/* | CRUD templates/versões + promote | Média | templates_* | | v1/api-keys/route.ts | W: criação rotação API key (serviço) | Média | — | | v1/groups/route.ts | R: dados grupos (Prisma/serviço) | Média | — | | v1/messages/send/route.ts | W: MessageService | Média | — | | v1/messages/opt-in/route.ts | Via cacheAside | Baixa | optin_status | | v1/numbers/[id]/route.ts | R: tenant, apiKey (Promise.all) | Baixa | — | | v1/numbers/[id]/status/route.ts | R: estado número (Prisma) | Baixa | — | | v1/projects/route.ts | R: projetos API pública | Baixa | — | | v1/templates/* | R: templates API pública | Baixa | cacheAside nas rotas GET | | whatsapp-instances/route.ts | R: user.findUnique + serviço instâncias | Média | — | | whatsapp-instances/[id]/route.ts | R/W: instância | Média | — | | Rotas só com serviços (sem prisma no route.ts) | | | | | webhooks/route.ts | WebhookService | Média | cacheAside + invalidação | | webhooks/[id]/route.ts | WebhookService | Média | invalidação | | logs/optins/route.ts | WhatsAppTransactionalOptInService | Média | logs_optins | | analytics/dashboard/route.ts | MessageService.getAnalyticsDashboard | Alta | analytics_charts | | v1/analytics/dashboard/route.ts | idem | Alta | idem | | analytics/ask-ai/route.ts | idem + chamada IA | Alta (DB + IA) | analytics_charts | | internal/onboarding/assets/route.ts | ensureOnboardingAssets | Média | onboarding_assets | | internal/messages/cancel-scheduled/route.ts | MessageService.cancelScheduledForTenant + $executeRaw interno | Média | invalidação logs_messages | | v1/messages/cancel/route.ts | idem | Média | invalidação logs_messages |


Fullstack — services/

| Ficheiro | Operações principais (modelos) | Peso | |----------|-------------------------------|------| | message.service.ts | project, apiKey, templateVersion, message (create, find, update, list+count, groupBy), $transaction; rawMessageListCount / rawMessageListIds; queryAnalyticsPeriodTotals / queryAnalyticsDailyBuckets; cancelScheduledMessageAtomicUpdate | Alta | | webhook.service.ts | webhook.findMany, $transaction create/update com webhookWhatsAppInstance, webhook.delete, webhookLog.create/findMany | Média | | whatsapp-instance.service.ts | whatsAppInstance CRUD + updates | Média | | api-key.service.ts | project, apiKey | Média | | template.service.ts | project, template, templateVersion (CRUD extensivo) | Média | | auto-recharge.service.ts | autoRechargeSettings, billing | Baixa–média | | payment.service.ts | subscription, packagePurchase, Stripe sync | Média | | billing-event.service.ts | billingEvent.create | Baixa | | subscription-access.service.ts | subscription, limits | Baixa | | rate-limit.service.ts | subscription, message.count, packagePurchase.aggregate, autoRechargeSettings, message.count (diário) | Alta em tenants com muitas mensagens | | monthly-usage-alert.service.ts | monthlyUsageAlert, tenant, user | Média | | pilot-status-whatsapp-notify.service.ts | user.findMany | Baixa | | whatsapp-transactional-optin.service.ts | opt-ins / listagens | Média | | whatsapp-webhook-group-delivery-sync.service.ts | sync entrega grupos | Média |


Fullstack — lib/ e modules/*/adapters/prisma

| Ficheiro | R/W | Peso | Notas | |----------|-----|------|--------| | message-analytics-aggregates.ts | R: $queryRaw agregados e buckets por dia | Alta | Usado por MessageService.getAnalyticsDashboard | | message-prisma-text-status-raw.ts | R: $queryRaw count, ids, listagens com compat status::text | Alta | Fallback quando enum MessageStatus em drift | | message-cancel-scheduled-update.ts | W: $executeRaw update atómico cancelamento | Média | | | prisma-cache-invalidation.ts | R: várias queries para resolver tenantId | Média | Disparada em mutations | | app-session.ts | R: user, tenant, project | Baixa | Por pedido autenticado | | auth.ts | R/W: account, tenant, subscription, phoneNumberOtp, user | Média | Fluxos OAuth / telefone | | environment-permissions.ts | R/W: project | Baixa | | | onboarding/ensure-onboarding-assets.ts | R/W: tenant, project, user, template | Média | | | prisma-analytics.repository.ts | R: message.count x6 ou $queryRaw count; message.findMany+count; raw logs | Alta | | | prisma-api-key.repository.ts | CRUD apiKey | Média | | | prisma-subscription.repository.ts | subscription | Baixa | | | prisma-customer-webhook.repository.ts | webhook | Média | | | prisma-label.repository.ts | label, labelContact | Média | | | prisma-whatsapp-instance.repository.ts | whatsAppInstance | Média | |


Worker — ficheiros de produção

| Ficheiro | R/W resumido | Peso | Cache | |----------|-------------|------|--------| | send-message.ts | message.findUnique (include grande); messageAttempt.count; message.update (muitos ramos); messageSendRequest.findUnique/update; whatsAppInstance.findUnique/updateMany; $transaction (marketing jitter / irregular batch); leituras extra message.findUnique | Alta | Redis lock / marketing keys apenas | | message-reconciler.ts | R: $queryRaw / message.findMany (fila QUEUED); W: message.updateMany; R: messageSendRequest.findMany; R: messageAttempt.groupBy | Alta | — | | sent-message-reconciler.ts | R: $queryRaw/message.findMany; R: evolutionWebhookEvent.findMany; W: message.update (várias) | Alta | — | | delivered-read-reconciler.ts | R: $queryRaw/message.findMany; W: message.updateMany | Alta | — | | sent-delivery-check.ts | R: message.findUnique; W: message.updateMany | Média | Redis resentKey | | sent-delivery-retry.ts | R: $queryRaw/message.findMany; W: $executeRaw + message.updateMany; R: messageSendRequest.findMany | Alta | Redis contadores | | ingestion-reconciler.ts | R: evolutionIngestionEvent.findMany; R: evolutionWebhookEvent.findFirst; W: evolutionIngestionEvent.update | Média | Redis done | | rabbitmq/evolution-consumer.ts | R: evolutionIngestionEvent.findUnique; W: $executeRaw, evolutionWebhookEvent.create, evolutionIngestionEvent.update | Média | Redis lock | | rabbitmq/evolution-go-consumer.ts | W: evolutionWebhookEvent.create | Baixa | — | | all-instances-healthcheck.ts | R: whatsAppInstance.findMany/findUnique; W: whatsAppInstance.updateMany | Alta (muitas instâncias) | Redis alertas | | recovery-sweep.ts | R: whatsAppInstance.findMany; W: whatsAppInstance.update | Média | — | | whatsapp.ts | W: whatsAppInstance.updateMany; R: user.findMany | Média | — | | labels-upsert.ts | W: label.upsert; R/W: labelContact | Baixa | — | | evolution-status.ts | R: evolutionMessageJid.findMany | Média | — | | list-desired-instance-names.ts | R: whatsAppInstance.findMany | Média | Sim — cache Redis opcional | | index.ts | R/W: subscription, billingEvent, apiKey, message.updateMany, labelContact.updateMany, whatsAppInstance, tenant, user, subscription.findMany | Média–alta (updateMany globais) | Redis fila/worker | | modules/messaging/adapters/prisma/prisma-message.repository.ts | message, messageSendRequest, messageAttempt | Média | — | | modules/whatsapp-instance/adapters/prisma/prisma-whatsapp-instance.repository.ts | whatsAppInstance, user | Média | — | | modules/billing-subscription/adapters/prisma/prisma-subscription.repository.ts | subscription | Baixa | — | | bootstrap/shutdown.ts | W: prisma.$disconnect() | — | — |


Diagrama (fluxo simplificado fullstack)

flowchart LR
  subgraph http [Next_API_Route]
    R[GET_handler]
  end
  subgraph cache [Redis_cacheAside]
    CA[cacheAside]
  end
  subgraph db [Prisma_Postgres]
    P[queries]
  end
  R --> CA
  CA -->|miss| P
  CA -->|hit| R
  P -->|mutation_listener| INV[invalidateTenantDomain]
  INV --> cache

Apêndice — ficheiros de teste com Prisma

Pesquisa global inclui dezenas de ficheiros em **/test/** e *.test.ts (fixtures, mocks). Para listagem exata, executar no repositório:

find apps/fullstack/src apps/worker/src \( -path '*/test/*' -o -name '*.test.ts' \) \
  -name '*.ts' -exec grep -lE 'prisma\\.' {} \;

Manutenção

Ao adicionar endpoints ou jobs:

  1. Incluir neste documento ou atualizar tabelas.
  2. Se usar cacheAside, registar domínio em cache-domains.ts e mapear invalidação em prisma-cache-invalidation.ts quando aplicável.