feat(shared): contrato Zod PingResponse em @sar/api-interface (Frente C)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +1,13 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import type { PingResponse } from '@sar/api-interface';
|
||||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||||
|
|
||||||
// Endpoint de verificação de fundação:
|
// Endpoint de verificação de fundação:
|
||||||
// - confirma que CLS está populando workspaceId + requestId;
|
// - confirma que CLS está populando workspaceId + requestId;
|
||||||
// - serve como alvo do healthcheck do docker compose / smoke test;
|
// - serve como alvo do healthcheck do docker compose / smoke test;
|
||||||
// - usado pela Web (Frente B) para validar conectividade real.
|
// - usado pela Web (Frente B) para validar conectividade real.
|
||||||
|
// Contrato: @sar/api-interface · PingResponseSchema (zod).
|
||||||
interface PingResponse {
|
|
||||||
status: 'ok';
|
|
||||||
service: string;
|
|
||||||
version: string;
|
|
||||||
workspaceId: string;
|
|
||||||
requestId: string;
|
|
||||||
uptimeSeconds: number;
|
|
||||||
now: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller({ path: 'ping' })
|
@Controller({ path: 'ping' })
|
||||||
export class PingController {
|
export class PingController {
|
||||||
|
|||||||
@@ -290,6 +290,35 @@
|
|||||||
- **Dev server:** http://localhost:4200/ servindo com title, theme-color, lang pt-BR corretos
|
- **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**
|
- **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
|
## About This Folder
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"types": "./src/index.d.ts",
|
"types": "./src/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"zod": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './lib/api-interface';
|
export * from './lib/ping.contract';
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { apiInterface } from './api-interface';
|
|
||||||
|
|
||||||
describe('apiInterface', () => {
|
|
||||||
it('should work', () => {
|
|
||||||
expect(apiInterface()).toEqual('api-interface');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function apiInterface(): string {
|
|
||||||
return 'api-interface';
|
|
||||||
}
|
|
||||||
43
libs/shared/api-interface/src/lib/ping.contract.spec.ts
Normal file
43
libs/shared/api-interface/src/lib/ping.contract.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
24
libs/shared/api-interface/src/lib/ping.contract.ts
Normal file
24
libs/shared/api-interface/src/lib/ping.contract.ts
Normal file
@@ -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<typeof PingResponseSchema>;
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -305,6 +305,9 @@ importers:
|
|||||||
tslib:
|
tslib:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
|
zod:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 4.4.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user