From ce5494aa8b36d7fe8367b32303b74890f8062ef3 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 May 2026 18:51:35 +0000 Subject: [PATCH] feat(shared): contrato Zod PingResponse em @sar/api-interface (Frente C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Primeiro contrato compartilhado API↔Web. Lib stays framework-free: nestjs-zod (createZodDto) fica fora — Web vai consumir esta mesma lib e não pode arrastar dependência backend. - PingResponseSchema + type PingResponse (z.infer) em ping.contract.ts - 6 testes vitest (1 happy + 5 rejeições: status, uuid, uptime, datetime, workspaceId) - ping.controller importa PingResponse via @sar/api-interface - placeholders Nx api-interface.{ts,spec.ts} removidos - design-log atualizado com decisão arquitetural e pegadinhas da sessão Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/app/ping/ping.controller.ts | 12 +----- design-artifacts/_progress/00-design-log.md | 29 +++++++++++++ libs/shared/api-interface/package.json | 3 +- libs/shared/api-interface/src/index.ts | 2 +- .../src/lib/api-interface.spec.ts | 7 --- .../api-interface/src/lib/api-interface.ts | 3 -- .../src/lib/ping.contract.spec.ts | 43 +++++++++++++++++++ .../api-interface/src/lib/ping.contract.ts | 24 +++++++++++ pnpm-lock.yaml | 3 ++ 9 files changed, 104 insertions(+), 22 deletions(-) delete mode 100644 libs/shared/api-interface/src/lib/api-interface.spec.ts delete mode 100644 libs/shared/api-interface/src/lib/api-interface.ts create mode 100644 libs/shared/api-interface/src/lib/ping.contract.spec.ts create mode 100644 libs/shared/api-interface/src/lib/ping.contract.ts diff --git a/apps/api/src/app/ping/ping.controller.ts b/apps/api/src/app/ping/ping.controller.ts index 0663a69..f65b4bf 100644 --- a/apps/api/src/app/ping/ping.controller.ts +++ b/apps/api/src/app/ping/ping.controller.ts @@ -1,21 +1,13 @@ import { Controller, Get } from '@nestjs/common'; import { ClsService } from 'nestjs-cls'; +import type { PingResponse } from '@sar/api-interface'; import type { WorkspaceClsStore } from '../workspace/workspace.types'; // Endpoint de verificação de fundação: // - confirma que CLS está populando workspaceId + requestId; // - serve como alvo do healthcheck do docker compose / smoke test; // - usado pela Web (Frente B) para validar conectividade real. - -interface PingResponse { - status: 'ok'; - service: string; - version: string; - workspaceId: string; - requestId: string; - uptimeSeconds: number; - now: string; -} +// Contrato: @sar/api-interface · PingResponseSchema (zod). @Controller({ path: 'ping' }) export class PingController { diff --git a/design-artifacts/_progress/00-design-log.md b/design-artifacts/_progress/00-design-log.md index 6c9c5db..0e64809 100644 --- a/design-artifacts/_progress/00-design-log.md +++ b/design-artifacts/_progress/00-design-log.md @@ -290,6 +290,35 @@ - **Dev server:** http://localhost:4200/ servindo com title, theme-color, lang pt-BR corretos - **Pendentes próxima sessão:** Frente A (API foundation), C (Zod contracts), D (ESLint+boundaries), Docker permissions, **abrir browser e validar visualmente** +### 2026-05-27 — Frente C (Zod contracts shared) + Docker dev healthy CONCLUÍDA ✅ +- **Escopo da sessão:** 1+2 do roadmap pendente — subir compose dev + criar primeiro contrato Zod compartilhado API↔Web. +- **`libs/shared/api-interface/` reescrita:** + - `src/lib/ping.contract.ts` — `PingResponseSchema` (z.object com `status: z.literal('ok')`, `requestId: z.uuid()`, `now: z.iso.datetime()`, etc.) + tipo inferido `PingResponse`. Comentário registra a decisão arquitetural abaixo. + - `src/lib/ping.contract.spec.ts` — 6 testes vitest (1 happy + 5 rejeições: status, uuid, uptime negativo, datetime inválido, workspaceId vazio). + - `src/index.ts` — reaponta para `ping.contract`. Placeholders `api-interface.{ts,spec.ts}` deletados. + - `package.json` — `+zod: catalog:` (sai do dependency-checks do Nx limpo). +- **Decisão arquitetural — lib stays framework-free:** `createZodDto` (nestjs-zod) **NÃO** vai pra `@sar/api-interface`. A Web vai consumir essa mesma lib e não pode arrastar dependência de framework backend. A lib carrega só Zod schema + tipo `z.infer`; quando precisar de DTO-class pro pipe, fica na camada API (`apps/api`). Alinha com CODING-RULES §06 ("schema é o contrato; DTO é a classe que o expõe"). +- **`apps/api/src/app/ping/ping.controller.ts`** — `import type { PingResponse } from '@sar/api-interface'`, interface inline removida. Comportamento idêntico (CLS, uptime, version). +- **Validação:** `nx run-many -t lint build -p api,api-interface` ✅ · `nx run api-interface:test` 6/6 ✅. +- **Pegadinha Postgres 18+ encontrada e fixada (canon JCS):** + - `pnpm dev:up` deu container `sar-postgres Restarting (1)` com erro "in 18+, these Docker images are configured to store database data in a format which is compatible with pg_ctlcluster (specifically, using major-version-specific directory names). Counter to that, there appears to be PostgreSQL data in: /var/lib/postgresql/data (unused mount/volume)". + - Causa: Postgres 18+ mudou o layout do mount canônico de `/var/lib/postgresql/data` para `/var/lib/postgresql` (dados em subdir `18/main`) para suportar `pg_upgrade --link` sem boundary issues. Ref: `docker-library/postgres#1259`. + - Fix: `docker-compose.dev.yml` postgres.volumes → `- sar-postgres-data:/var/lib/postgresql`. Comentário inline explicando o porquê. + - Volume `sar-postgres-data` removido (`docker volume rm`) e compose religado limpo. Sem perda de dados (era primeiro boot). +- **Pegadinha permissão Docker (registro pra próximas máquinas dev):** + - Usuário fora do grupo `docker` → `permission denied` no `/var/run/docker.sock`. Fix: `sudo usermod -aG docker $USER` + `newgrp docker` (efeito imediato na sub-shell) ou logout/login para fixar em todos terminais novos. Aceitável em desktop dev pessoal; em servidor compartilhado, **não** (grupo docker ≡ root local). +- **Stack dev validada healthy:** + - `sar-postgres` (18-alpine) 5432 ✅ — BDs `sar_master` + `sar_workspace_dev`, extensions `pgcrypto`+`uuid-ossp` no workspace e `pgcrypto`+`plpgsql` no master. + - `sar-valkey` (8-alpine) 6379 ✅ — `+PONG`. + - `sar-minio` (latest) 9000+9001 ✅ — `/minio/health/live` HTTP 200; console UI em http://localhost:9001. + - `sar-mailpit` (latest) 1025+8025 ✅ — web UI em http://localhost:8025. +- **Pendente próxima sessão (em ordem recomendada):** + 1. **Web→API integração ponta-a-ponta** — `apps/web` chama `/api/v1/ping` via TanStack Query + `PingResponseSchema.parse(...)` na response. Fecha o loop B+C; prova que o contrato Zod funciona dos dois lados. + 2. **Frente D — ESLint boundaries** (tags Nx `scope:* · type:* · domain:*`) + Husky + gitleaks. + 3. **Phase 3 WDS — PRD** via `/bmad-prd create` antes de qualquer modelagem de domínio. + 4. **Master-login stub + WorkspacePrismaPool** (frente arquitetural pesada — depende de #3). + 5. **OpenTelemetry SDK** plugar quando entrar em catálogo (stub atual mantém posição). + --- ## About This Folder diff --git a/libs/shared/api-interface/package.json b/libs/shared/api-interface/package.json index c2ac6a3..f130f14 100644 --- a/libs/shared/api-interface/package.json +++ b/libs/shared/api-interface/package.json @@ -6,6 +6,7 @@ "main": "./src/index.js", "types": "./src/index.d.ts", "dependencies": { - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "zod": "catalog:" } } diff --git a/libs/shared/api-interface/src/index.ts b/libs/shared/api-interface/src/index.ts index 35d5e3b..c1227ab 100644 --- a/libs/shared/api-interface/src/index.ts +++ b/libs/shared/api-interface/src/index.ts @@ -1 +1 @@ -export * from './lib/api-interface'; +export * from './lib/ping.contract'; diff --git a/libs/shared/api-interface/src/lib/api-interface.spec.ts b/libs/shared/api-interface/src/lib/api-interface.spec.ts deleted file mode 100644 index 70d5a40..0000000 --- a/libs/shared/api-interface/src/lib/api-interface.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { apiInterface } from './api-interface'; - -describe('apiInterface', () => { - it('should work', () => { - expect(apiInterface()).toEqual('api-interface'); - }); -}); diff --git a/libs/shared/api-interface/src/lib/api-interface.ts b/libs/shared/api-interface/src/lib/api-interface.ts deleted file mode 100644 index 851b2f1..0000000 --- a/libs/shared/api-interface/src/lib/api-interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function apiInterface(): string { - return 'api-interface'; -} diff --git a/libs/shared/api-interface/src/lib/ping.contract.spec.ts b/libs/shared/api-interface/src/lib/ping.contract.spec.ts new file mode 100644 index 0000000..9d10c60 --- /dev/null +++ b/libs/shared/api-interface/src/lib/ping.contract.spec.ts @@ -0,0 +1,43 @@ +import { PingResponseSchema, type PingResponse } from './ping.contract'; + +describe('PingResponseSchema', () => { + const validPayload: PingResponse = { + status: 'ok', + service: 'sar-api', + version: '0.1.0', + workspaceId: 'dev-workspace', + requestId: '550e8400-e29b-41d4-a716-446655440000', + uptimeSeconds: 42, + now: '2026-05-27T12:34:56.000Z', + }; + + it('aceita payload válido', () => { + const parsed = PingResponseSchema.parse(validPayload); + expect(parsed).toEqual(validPayload); + }); + + it('rejeita status diferente de "ok"', () => { + const bad = { ...validPayload, status: 'degraded' }; + expect(() => PingResponseSchema.parse(bad)).toThrow(); + }); + + it('rejeita requestId que não é UUID', () => { + const bad = { ...validPayload, requestId: 'not-a-uuid' }; + expect(() => PingResponseSchema.parse(bad)).toThrow(); + }); + + it('rejeita uptimeSeconds negativo', () => { + const bad = { ...validPayload, uptimeSeconds: -1 }; + expect(() => PingResponseSchema.parse(bad)).toThrow(); + }); + + it('rejeita "now" que não é ISO datetime', () => { + const bad = { ...validPayload, now: '2026-05-27' }; + expect(() => PingResponseSchema.parse(bad)).toThrow(); + }); + + it('rejeita workspaceId vazio', () => { + const bad = { ...validPayload, workspaceId: '' }; + expect(() => PingResponseSchema.parse(bad)).toThrow(); + }); +}); diff --git a/libs/shared/api-interface/src/lib/ping.contract.ts b/libs/shared/api-interface/src/lib/ping.contract.ts new file mode 100644 index 0000000..81087b7 --- /dev/null +++ b/libs/shared/api-interface/src/lib/ping.contract.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +// Contrato canônico do endpoint GET /api/v1/ping. +// +// Consumido por: +// - apps/api: PingController retorna PingResponse (saída tipada). +// - apps/web: TanStack Query parseia a resposta via PingResponseSchema.parse(...) +// para validar o contrato em runtime — qualquer drift no servidor falha alto. +// +// CODING-RULES §06: o schema Zod É o contrato. DTO/classes (createZodDto) ficam +// na camada API quando precisar do pipe de validação; aqui carregamos só o que +// é compartilhável entre Node e Browser. + +export const PingResponseSchema = z.object({ + status: z.literal('ok'), + service: z.string().min(1), + version: z.string().min(1), + workspaceId: z.string().min(1), + requestId: z.uuid(), + uptimeSeconds: z.number().int().nonnegative(), + now: z.iso.datetime(), +}); + +export type PingResponse = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0edf060..8f80813 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,9 @@ importers: tslib: specifier: ^2.3.0 version: 2.8.1 + zod: + specifier: 'catalog:' + version: 4.4.3 packages: