@ds9-solutions/mcp 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +43 -0
- package/README.md +53 -0
- package/content/agents/code-reviewer.agent.md +52 -22
- package/content/agents/db-analyser.agent.md +121 -0
- package/content/agents/security-reviewer.agent.md +119 -37
- package/content/agents/test-writer.agent.md +84 -35
- package/content/skills/dart/SKILL.md +139 -0
- package/content/skills/dart/examples/entity.dart +37 -0
- package/content/skills/dart/examples/error_handling.dart +36 -0
- package/content/skills/dart/examples/order_test.dart +34 -0
- package/content/skills/dart/examples/repository.dart +57 -0
- package/content/skills/dart/references/checklist.md +12 -0
- package/content/skills/dart/references/documentation.md +26 -0
- package/content/skills/dart/references/naming.md +27 -0
- package/content/skills/dart-frog/SKILL.md +208 -0
- package/content/skills/dart-frog/examples/auth_middleware.dart +50 -0
- package/content/skills/dart-frog/examples/controller.dart +75 -0
- package/content/skills/dart-frog/examples/dto.dart +41 -0
- package/content/skills/dart-frog/examples/error_response.dart +35 -0
- package/content/skills/dart-frog/examples/exceptions.dart +60 -0
- package/content/skills/dart-frog/examples/middleware_composition.dart +34 -0
- package/content/skills/dart-frog/examples/model.dart +20 -0
- package/content/skills/dart-frog/examples/port.dart +17 -0
- package/content/skills/dart-frog/examples/repository.dart +56 -0
- package/content/skills/dart-frog/examples/route_collection.dart +25 -0
- package/content/skills/dart-frog/examples/route_item.dart +24 -0
- package/content/skills/dart-frog/examples/route_test.dart +33 -0
- package/content/skills/dart-frog/examples/service.dart +51 -0
- package/content/skills/dart-frog/examples/service_test.dart +55 -0
- package/content/skills/dart-frog/references/checklist.md +31 -0
- package/content/skills/dart-frog/references/error-handling.md +48 -0
- package/content/skills/dart-frog/references/fpdart-taskeither.md +53 -0
- package/content/skills/dart-frog/references/layers.md +43 -0
- package/content/skills/dart-frog/references/middlewares-di.md +45 -0
- package/content/skills/dart-frog/references/project-structure.md +67 -0
- package/content/skills/dart-frog/references/routing.md +53 -0
- package/content/skills/dart-frog/references/testing.md +24 -0
- package/content/skills/docker/SKILL.md +3 -3
- package/content/skills/driftorm/SKILL.md +249 -0
- package/content/skills/flutter/SKILL.md +419 -0
- package/content/skills/hex-pattern-dart/SKILL.md +83 -0
- package/content/skills/hex-pattern-dart/examples/adapter-in.md +24 -0
- package/content/skills/hex-pattern-dart/examples/adapter-out.md +20 -0
- package/content/skills/hex-pattern-dart/examples/errors.md +25 -0
- package/content/skills/hex-pattern-dart/examples/port.md +11 -0
- package/content/skills/hex-pattern-dart/examples/service.md +20 -0
- package/content/skills/hex-pattern-dart/examples/structure-tree.md +28 -0
- package/content/skills/hex-pattern-dart/references/anti-patterns.md +16 -0
- package/content/skills/hex-pattern-dart/references/checklist.md +34 -0
- package/content/skills/hex-pattern-dart/references/dependency-injection.md +32 -0
- package/content/skills/hex-pattern-dart/references/error-layers.md +27 -0
- package/content/skills/hex-pattern-dart/references/layers-and-dependency-rule.md +29 -0
- package/content/skills/hex-pattern-dart/references/persistence.md +23 -0
- package/content/skills/hex-pattern-dart/references/ports-and-adapters.md +33 -0
- package/content/skills/hex-pattern-dart/references/testing.md +30 -0
- package/content/skills/mysql/SKILL.md +3 -3
- package/content/skills/typescript/SKILL.md +4 -4
- package/dist/src/content.js +10 -2
- package/dist/src/init.js +1 -0
- package/dist/src/search.js +20 -12
- package/dist/src/server.js +60 -4
- package/dist/src/skill-files.js +125 -0
- package/package.json +2 -2
- package/content/agents/db-specialist.agent.md +0 -78
- package/content/agents/nestjs-api-designer.agent.md +0 -61
- package/content/skills/hexagonal/SKILL.md +0 -156
- package/content/skills/jest-testing/SKILL.md +0 -88
- package/content/skills/nestjs/SKILL.md +0 -100
- package/content/skills/typeorm/SKILL.md +0 -119
- package/dist/test/mcp.test.js +0 -112
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dart-frog
|
|
3
|
+
description: Diretrizes DS9 para backends com Dart Frog em arquitetura hexagonal (domain/adapter por contexto) — rotas finas com 405 obrigatório, injeção de dependência exclusivamente por middleware, `fpdart` com `TaskEither`/`TaskEither.tryCatch` como retorno entre camadas e exceções por camada (BusinessException, ClientException, MiddlewareException). Persistência é agnóstica (use a skill de persistência do projeto). Use ao criar, editar, revisar ou testar rotas, `_middleware.dart`, controllers, services, ports, repositories ou DTOs em um projeto Dart Frog (pasta `routes/`, comandos `dart_frog dev/build`), ou quando o usuário pedir "backend Dart", "API Dart Frog" ou "arquitetura hexagonal em Dart".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dart Frog — Diretrizes DS9
|
|
7
|
+
|
|
8
|
+
Padrão DS9 para APIs em **Dart Frog** com **arquitetura hexagonal** (ports & adapters), organizada por contexto de negócio. Complementa as skills `dart` (nomenclatura, `///`, OOP, null safety) e `hex-pattern-dart` (camadas, ports/adapters, erros): siga as três. A persistência é **agnóstica** aqui; para o banco/ORM do projeto, consulte a skill de persistência correspondente (`list_skills` / `recommend_for_task`).
|
|
9
|
+
|
|
10
|
+
## 0. Pré-requisitos (verifique antes de gerar código)
|
|
11
|
+
|
|
12
|
+
1. Confirme no `pubspec.yaml` do projeto as dependências `dart_frog` e **`fpdart`**.
|
|
13
|
+
2. **Se `fpdart` não estiver instalado, informe ao usuário que ele é obrigatório para seguir o padrão DS9** e peça autorização para instalar (`dart pub add fpdart`). Não gere código fora do padrão para contornar a ausência.
|
|
14
|
+
3. Use as versões já fixadas no `pubspec.yaml`; não atualize dependências sem pedido explícito.
|
|
15
|
+
4. Preserve a estrutura existente em projetos pré-existentes; aplique o padrão integralmente em código novo e no que for explicitamente solicitado, sem refatorar o restante.
|
|
16
|
+
|
|
17
|
+
## 1. Regras inegociáveis
|
|
18
|
+
|
|
19
|
+
1. **`fpdart` + `TaskEither`:** todo contrato de camada (port, service, repository, controller) que pode falhar **retorna `TaskEither<Falha, T>`**, e a operação assíncrona é envolvida com **`TaskEither.tryCatch`**. Nunca lance exceção para fora de uma camada nem converta falha em sucesso vazio.
|
|
20
|
+
2. **Injeção só por middleware:** dependências são compostas em `_middleware.dart` com `provider<T>` e lidas com `context.read<T>()`. Nada de instanciar service/repository/controller no handler, sem service locator, sem singleton global fora do middleware.
|
|
21
|
+
3. **405 sempre:** todo handler faz `switch` em `context.request.method` e termina com `case _: return Response(statusCode: HttpStatus.methodNotAllowed);`.
|
|
22
|
+
4. **Camadas de erro fixas:** `BusinessException` (service/repository), `ClientException` (controller), `MiddlewareException` (middleware). Nomes e responsabilidades não mudam entre projetos.
|
|
23
|
+
5. **Persistência agnóstica:** o domínio só conhece **ports**; o acesso a dados fica em `adapter/repository`, sem citar ORM/driver nesta skill.
|
|
24
|
+
|
|
25
|
+
## 2. Estrutura
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
<projeto>/
|
|
29
|
+
├── routes/ # roteador (arquivos = rotas); só despacho HTTP
|
|
30
|
+
│ ├── _middleware.dart # raiz: CORS, infraestrutura compartilhada
|
|
31
|
+
│ ├── api/_middleware.dart # autenticação do grupo
|
|
32
|
+
│ └── api/orders/
|
|
33
|
+
│ ├── _middleware.dart # composição do contexto (providers)
|
|
34
|
+
│ ├── index.dart # /api/orders
|
|
35
|
+
│ └── [id].dart # /api/orders/:id
|
|
36
|
+
├── lib/
|
|
37
|
+
│ ├── <contexto>/
|
|
38
|
+
│ │ ├── domain/{models,ports,services}/
|
|
39
|
+
│ │ └── adapter/{controllers,dtos,repository}/
|
|
40
|
+
│ └── common/{exception,http,middleware,environment,logger,models}/
|
|
41
|
+
├── test/ # espelha lib/ e routes/
|
|
42
|
+
└── main.dart # só init() e run()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Estrutura completa e variações (`application/use_case`, adapters de integração): `references/project-structure.md`.
|
|
46
|
+
|
|
47
|
+
## 3. Camadas e regra de dependência
|
|
48
|
+
|
|
49
|
+
| Camada | Responsabilidade | Retorno | Falha |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `routes/**` | despachar método HTTP, ler `controller` do contexto, converter resultado em `Response` | `Future<Response>` | converte `Left` em JSON de erro |
|
|
52
|
+
| `adapter/controllers` | ler body/query/path, validar formato (DTO), chamar service, converter modelo → DTO | `TaskEither<ClientException, T>` | `ClientException` |
|
|
53
|
+
| `domain/services` | regras de negócio, autorização de domínio, composição de ports | `TaskEither<BusinessException, T>` | `BusinessException` |
|
|
54
|
+
| `domain/ports` | contratos (`abstract interface class`) | `TaskEither<BusinessException, T>` | `BusinessException` |
|
|
55
|
+
| `adapter/repository` | implementa o port e acessa a persistência | `TaskEither<BusinessException, T>` | `BusinessException` |
|
|
56
|
+
| `middlewares` | autenticar, compor providers, encerrar com erro | `Handler` | `MiddlewareException` |
|
|
57
|
+
|
|
58
|
+
Dependências apontam para dentro: `routes → controller → service → port ← repository`.
|
|
59
|
+
|
|
60
|
+
- `domain/` **nunca** importa `dart_frog`, SDK, driver/ORM, `adapter/`, `RequestContext` nem `Response`. Pode importar `fpdart`, `equatable`, `json_annotation` e `common/exception`.
|
|
61
|
+
- Contextos se comunicam por **port ou service público**, nunca pelo repository concreto de outro contexto, sem dependência circular.
|
|
62
|
+
- Mapeamento: linha de banco → modelo no repository; modelo → DTO no controller/DTO. Modelos de domínio não conhecem DTOs nem classes geradas.
|
|
63
|
+
|
|
64
|
+
Detalhes e responsabilidades por camada: `references/layers.md`.
|
|
65
|
+
|
|
66
|
+
## 4. `TaskEither` e `tryCatch`
|
|
67
|
+
|
|
68
|
+
```dart
|
|
69
|
+
// repository: toda chamada que pode falhar entra em tryCatch
|
|
70
|
+
TaskEither<BusinessException, Order?> findById(String id) {
|
|
71
|
+
return TaskEither.tryCatch(
|
|
72
|
+
() => _dataSource.findById(id),
|
|
73
|
+
(error, stackTrace) => BusinessException(
|
|
74
|
+
domainName: 'order',
|
|
75
|
+
statusCode: HttpStatus.internalServerError,
|
|
76
|
+
message: 'Falha ao consultar o pedido.',
|
|
77
|
+
error: error,
|
|
78
|
+
stackTrace: stackTrace,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// service: compõe com flatMap/map/mapLeft; regra inválida vira TaskEither.left
|
|
84
|
+
TaskEither<BusinessException, Order> getById(String id) =>
|
|
85
|
+
_repository.findById(id).flatMap(
|
|
86
|
+
(order) => order == null
|
|
87
|
+
? TaskEither.left(_notFound(id))
|
|
88
|
+
: TaskEither.right(order),
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- Prefira `TaskEither.tryCatch`, `map`, `flatMap`, `mapLeft`; execute (`.run()`) **só na rota**.
|
|
93
|
+
- Não use `Exception` genérica como falha; não capture `Error`; não use `catch (_)` amplo.
|
|
94
|
+
- Encadeamentos de `TaskEither` são a exceção funcional aceita da skill `dart`; fora desse contrato, continue em OOP.
|
|
95
|
+
|
|
96
|
+
Guia completo: `references/fpdart-taskeither.md`.
|
|
97
|
+
|
|
98
|
+
## 5. Erros por camada
|
|
99
|
+
|
|
100
|
+
| Tipo | Camada | Campos |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `BusinessException` | service, repository | `domainName`, `statusCode`, `message` (seguro para o cliente), `error` (causa original, nunca exposta), `stackTrace` |
|
|
103
|
+
| `ClientException` | controller | `statusCode`, `message` |
|
|
104
|
+
| `MiddlewareException` | middleware | `statusCode`, `message`, `stackTrace?` |
|
|
105
|
+
|
|
106
|
+
- O controller converte `BusinessException` em `ClientException` preservando o `statusCode`; para `5xx`, use mensagem genérica.
|
|
107
|
+
- Formato único de erro: `{"error": {"code": "...", "message": "..."}}`.
|
|
108
|
+
- **Nunca** envie stack trace, SQL, causa original (`error`), caminho de arquivo ou detalhe de SDK ao cliente; registre no log sem dados sensíveis.
|
|
109
|
+
|
|
110
|
+
Status HTTP, formato e exemplos: `references/error-handling.md`.
|
|
111
|
+
|
|
112
|
+
## 6. Rotas (sempre 405)
|
|
113
|
+
|
|
114
|
+
```dart
|
|
115
|
+
Future<Response> onRequest(RequestContext context) async {
|
|
116
|
+
final controller = context.read<OrderController>();
|
|
117
|
+
|
|
118
|
+
switch (context.request.method) {
|
|
119
|
+
case HttpMethod.post:
|
|
120
|
+
final result = await controller.create(context).run();
|
|
121
|
+
return result.match(
|
|
122
|
+
ErrorResponse.fromClient,
|
|
123
|
+
(order) => Response.json(statusCode: HttpStatus.created, body: order.toJson()),
|
|
124
|
+
);
|
|
125
|
+
case _:
|
|
126
|
+
return Response(statusCode: HttpStatus.methodNotAllowed);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- Rota fina: sem regra de negócio, query, SDK ou instanciação de dependência.
|
|
132
|
+
- `index.dart` para o recurso, `[id].dart` para parâmetro (`onRequest(RequestContext context, String id)`); sem conflito entre `api.dart` e `api/index.dart`.
|
|
133
|
+
- Use `HttpStatus` de `dart:io` em vez de números mágicos.
|
|
134
|
+
|
|
135
|
+
Convenções do roteador, CLI e catálogo de grupos: `references/routing.md`.
|
|
136
|
+
|
|
137
|
+
## 7. Injeção de dependência por middleware
|
|
138
|
+
|
|
139
|
+
```dart
|
|
140
|
+
// routes/api/orders/_middleware.dart
|
|
141
|
+
OrderRepositoryPort? _repository;
|
|
142
|
+
OrderService? _service;
|
|
143
|
+
OrderController? _controller;
|
|
144
|
+
|
|
145
|
+
Handler middleware(Handler handler) {
|
|
146
|
+
return handler
|
|
147
|
+
.use(provider<OrderController>((c) => _controller ??= OrderController(c.read<OrderService>())))
|
|
148
|
+
.use(provider<OrderService>((c) => _service ??= OrderService(c.read<OrderRepositoryPort>())))
|
|
149
|
+
.use(provider<OrderRepositoryPort>((c) => _repository ??= OrderRepository(c.read<OrderDataSource>())));
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- Construtores recebem **ports/interfaces**; campos `final`.
|
|
154
|
+
- A resolução é de baixo para cima: se `B` depende de `A`, declare `.use(B).use(A)`.
|
|
155
|
+
- `provider` recria o valor a cada leitura; para recursos caros, faça cache em variável privada (`??=`).
|
|
156
|
+
- Infraestrutura compartilhada (conexão, configuração, rate limit) é provida no middleware raiz.
|
|
157
|
+
- Middleware **não** contém regra de negócio nem lê parâmetros de caminho por posição; autorização de recurso fica no service.
|
|
158
|
+
- Nunca `context.read` em models, ports, services ou repositories.
|
|
159
|
+
|
|
160
|
+
Composição por grupo, autenticação e ordem: `references/middlewares-di.md`.
|
|
161
|
+
|
|
162
|
+
## 8. Configuração, segurança e geração de código
|
|
163
|
+
|
|
164
|
+
- Variáveis de ambiente lidas em um ponto único (`common/environment`); nunca hardcodar segredos nem logar tokens ou dados pessoais. `String.fromEnvironment` não funciona no Dart Frog (use `Platform.environment`).
|
|
165
|
+
- Validação de formato no controller/DTO; de regra de negócio no service; falhas de persistência no repository.
|
|
166
|
+
- Nunca confie em `uid`, papel ou perfil enviados pelo cliente sem validar no servidor.
|
|
167
|
+
- Arquivos gerados (`*.g.dart`) não são editados à mão; regenere com `dart run build_runner build --delete-conflicting-outputs`.
|
|
168
|
+
- `main.dart` só define `init()`/`run()`; não compõe services.
|
|
169
|
+
|
|
170
|
+
## 9. Testes
|
|
171
|
+
|
|
172
|
+
- Service: fake/mock do port, sem banco real. Controller: entrada válida, inválida, `Right` e `Left` com status correto. Rota: `RequestContext` mockado, cobrindo os métodos suportados **e o 405**. Para criar/executar testes, use o agente `test-writer`.
|
|
173
|
+
- Estrutura em `test/` espelhando `lib/` e `routes/`. Detalhes: `references/testing.md`.
|
|
174
|
+
|
|
175
|
+
## 10. Checklist de review
|
|
176
|
+
|
|
177
|
+
- [ ] `fpdart` instalado; contratos retornam `TaskEither` com `tryCatch` nas operações que falham?
|
|
178
|
+
- [ ] Rota fina, com `case _` retornando 405?
|
|
179
|
+
- [ ] Dependências compostas só em `_middleware.dart`, nada instanciado no handler?
|
|
180
|
+
- [ ] `BusinessException` / `ClientException` / `MiddlewareException` nas camadas certas, sem vazar causa interna?
|
|
181
|
+
- [ ] `domain/` sem Dart Frog, SDK ou persistência concreta?
|
|
182
|
+
- [ ] Segredos e dados pessoais fora do código e dos logs; testes cobrindo regra nova e o 405?
|
|
183
|
+
|
|
184
|
+
Checklist detalhado: `references/checklist.md`.
|
|
185
|
+
|
|
186
|
+
## Não fazer
|
|
187
|
+
|
|
188
|
+
- Não instanciar service/repository/controller na rota ou usar service locator.
|
|
189
|
+
- Não retornar `Future` simples, `throw` ou `Exception` genérica nos contratos das camadas.
|
|
190
|
+
- Não colocar regra de negócio em rota, controller ou middleware.
|
|
191
|
+
- Não omitir o 405; não expor stack trace, SQL ou mensagens de SDK.
|
|
192
|
+
- Não referenciar ORM/driver no domínio nem nesta skill.
|
|
193
|
+
|
|
194
|
+
## 11. Arquivos complementares (leitura sob demanda)
|
|
195
|
+
|
|
196
|
+
Leia só o necessário (`list_skill_files` / `get_skill_file` com `skill="dart-frog"`; ou direto em `dart-frog/` sem as tools).
|
|
197
|
+
|
|
198
|
+
| Dúvida | Arquivo |
|
|
199
|
+
|---|---|
|
|
200
|
+
| Árvore de pastas, variações | `references/project-structure.md` |
|
|
201
|
+
| O que cada camada faz e não faz | `references/layers.md` |
|
|
202
|
+
| `TaskEither`, `tryCatch`, instalação do `fpdart` | `references/fpdart-taskeither.md` |
|
|
203
|
+
| Exceções, status e formato de erro | `references/error-handling.md` |
|
|
204
|
+
| Rotas, 405, CLI, grupos | `references/routing.md` |
|
|
205
|
+
| Middlewares, providers e autenticação | `references/middlewares-di.md` |
|
|
206
|
+
| Testes | `references/testing.md` |
|
|
207
|
+
| Checklist de review | `references/checklist.md` |
|
|
208
|
+
| Exemplos de código (exceções, model, port, service, repository, DTO, controller, middlewares, rotas, testes) | `examples/` |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Caminho: lib/common/middleware/auth_middleware.dart
|
|
2
|
+
// Autentica a requisição e provê o usuário; falhas são MiddlewareException.
|
|
3
|
+
import 'dart:io';
|
|
4
|
+
|
|
5
|
+
import 'package:dart_frog/dart_frog.dart';
|
|
6
|
+
|
|
7
|
+
import '../exception/exceptions.dart';
|
|
8
|
+
import '../http/error_response.dart';
|
|
9
|
+
|
|
10
|
+
/// Usuário autenticado disponibilizado aos handlers.
|
|
11
|
+
class AuthenticatedUser {
|
|
12
|
+
/// Cria o usuário autenticado.
|
|
13
|
+
const AuthenticatedUser({required this.id});
|
|
14
|
+
|
|
15
|
+
/// Identificador do usuário.
|
|
16
|
+
final String id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Port de verificação de credenciais (a implementação é um adapter).
|
|
20
|
+
abstract interface class TokenVerifierPort {
|
|
21
|
+
/// Retorna o usuário do [token] ou `null` se inválido.
|
|
22
|
+
Future<AuthenticatedUser?> verify(String token);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Exige `Authorization: Bearer <token>` e provê [AuthenticatedUser].
|
|
26
|
+
Middleware authMiddleware() {
|
|
27
|
+
return (handler) {
|
|
28
|
+
return (context) async {
|
|
29
|
+
final header = context.request.headers[HttpHeaders.authorizationHeader];
|
|
30
|
+
if (header == null || !header.startsWith('Bearer ')) {
|
|
31
|
+
return ErrorResponse.fromMiddleware(const MiddlewareException(
|
|
32
|
+
statusCode: HttpStatus.unauthorized,
|
|
33
|
+
message: 'Credenciais ausentes.',
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
final user = await context
|
|
38
|
+
.read<TokenVerifierPort>()
|
|
39
|
+
.verify(header.substring('Bearer '.length));
|
|
40
|
+
if (user == null) {
|
|
41
|
+
return ErrorResponse.fromMiddleware(const MiddlewareException(
|
|
42
|
+
statusCode: HttpStatus.unauthorized,
|
|
43
|
+
message: 'Credenciais inválidas.',
|
|
44
|
+
));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return handler(context.provide<AuthenticatedUser>(() => user));
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Caminho: lib/orders/adapter/controllers/order_controller.dart
|
|
2
|
+
import 'dart:io';
|
|
3
|
+
|
|
4
|
+
import 'package:dart_frog/dart_frog.dart';
|
|
5
|
+
import 'package:fpdart/fpdart.dart';
|
|
6
|
+
|
|
7
|
+
import '../../../common/exception/exceptions.dart';
|
|
8
|
+
import '../../domain/services/order_service.dart';
|
|
9
|
+
import '../dtos/order_dtos.dart';
|
|
10
|
+
|
|
11
|
+
/// Adapta HTTP para o domínio; recebido pronto via `context.read`.
|
|
12
|
+
class OrderController {
|
|
13
|
+
/// Cria o controller com o [_service] injetado pelo middleware.
|
|
14
|
+
const OrderController(this._service);
|
|
15
|
+
|
|
16
|
+
final OrderService _service;
|
|
17
|
+
|
|
18
|
+
/// POST /orders — cria um pedido para o usuário autenticado.
|
|
19
|
+
TaskEither<ClientException, OrderResponse> create(
|
|
20
|
+
RequestContext context,
|
|
21
|
+
String requesterId,
|
|
22
|
+
) {
|
|
23
|
+
return TaskEither<ClientException, Object?>.tryCatch(
|
|
24
|
+
() => context.request.json(),
|
|
25
|
+
(_, __) => const ClientException(
|
|
26
|
+
statusCode: HttpStatus.badRequest,
|
|
27
|
+
message: 'Body JSON inválido.',
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
.flatMap(_toDto)
|
|
31
|
+
.flatMap(
|
|
32
|
+
(dto) => _service
|
|
33
|
+
.create(ownerId: requesterId, total: dto.total!)
|
|
34
|
+
.mapLeft(_toClient),
|
|
35
|
+
)
|
|
36
|
+
.map(OrderResponse.fromDomain);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// GET /orders/:id — retorna um pedido do usuário autenticado.
|
|
40
|
+
TaskEither<ClientException, OrderResponse> getById(
|
|
41
|
+
String id,
|
|
42
|
+
String requesterId,
|
|
43
|
+
) {
|
|
44
|
+
return _service
|
|
45
|
+
.getOwnedById(id: id, requesterId: requesterId)
|
|
46
|
+
.mapLeft(_toClient)
|
|
47
|
+
.map(OrderResponse.fromDomain);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
TaskEither<ClientException, CreateOrderRequest> _toDto(Object? json) {
|
|
51
|
+
if (json is! Map<String, Object?>) {
|
|
52
|
+
return TaskEither.left(const ClientException(
|
|
53
|
+
statusCode: HttpStatus.badRequest,
|
|
54
|
+
message: 'O body deve ser um objeto JSON.',
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
final dto = CreateOrderRequest.fromJson(json);
|
|
58
|
+
final errors = dto.validate();
|
|
59
|
+
return errors.isEmpty
|
|
60
|
+
? TaskEither.right(dto)
|
|
61
|
+
: TaskEither.left(ClientException(
|
|
62
|
+
statusCode: HttpStatus.badRequest,
|
|
63
|
+
message: errors.join(' '),
|
|
64
|
+
));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ClientException _toClient(BusinessException failure) {
|
|
68
|
+
return ClientException(
|
|
69
|
+
statusCode: failure.statusCode,
|
|
70
|
+
message: failure.statusCode >= 500
|
|
71
|
+
? 'Erro interno. Tente novamente.'
|
|
72
|
+
: failure.message,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Caminho: lib/orders/adapter/dtos/order_dtos.dart (um DTO por arquivo em projetos reais)
|
|
2
|
+
import '../../domain/models/order.dart';
|
|
3
|
+
|
|
4
|
+
/// Entrada de criação de pedido.
|
|
5
|
+
class CreateOrderRequest {
|
|
6
|
+
/// Cria a requisição com o [total] informado.
|
|
7
|
+
const CreateOrderRequest({required this.total});
|
|
8
|
+
|
|
9
|
+
/// Converte o JSON recebido; campos ausentes ou inválidos ficam `null`.
|
|
10
|
+
factory CreateOrderRequest.fromJson(Map<String, Object?> json) {
|
|
11
|
+
final total = json['total'];
|
|
12
|
+
return CreateOrderRequest(total: total is num ? total.toDouble() : null);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Valor total informado pelo cliente.
|
|
16
|
+
final double? total;
|
|
17
|
+
|
|
18
|
+
/// Retorna as mensagens de validação de formato; vazio quando válido.
|
|
19
|
+
List<String> validate() => [
|
|
20
|
+
if (total == null) 'O campo "total" é obrigatório e numérico.',
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Saída pública de um pedido.
|
|
25
|
+
class OrderResponse {
|
|
26
|
+
/// Cria a resposta.
|
|
27
|
+
const OrderResponse({required this.id, required this.total});
|
|
28
|
+
|
|
29
|
+
/// Constrói a resposta a partir do modelo de domínio.
|
|
30
|
+
factory OrderResponse.fromDomain(Order order) =>
|
|
31
|
+
OrderResponse(id: order.id, total: order.total);
|
|
32
|
+
|
|
33
|
+
/// Identificador do pedido.
|
|
34
|
+
final String id;
|
|
35
|
+
|
|
36
|
+
/// Valor total do pedido.
|
|
37
|
+
final double total;
|
|
38
|
+
|
|
39
|
+
/// Serializa para JSON.
|
|
40
|
+
Map<String, Object?> toJson() => {'id': id, 'total': total};
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Caminho: lib/common/http/error_response.dart
|
|
2
|
+
import 'dart:io';
|
|
3
|
+
|
|
4
|
+
import 'package:dart_frog/dart_frog.dart';
|
|
5
|
+
|
|
6
|
+
import '../exception/exceptions.dart';
|
|
7
|
+
|
|
8
|
+
/// Converte falhas das camadas no formato único de erro da API.
|
|
9
|
+
abstract final class ErrorResponse {
|
|
10
|
+
/// Resposta para uma [ClientException].
|
|
11
|
+
static Response fromClient(ClientException failure) =>
|
|
12
|
+
_json(failure.statusCode, failure.message);
|
|
13
|
+
|
|
14
|
+
/// Resposta para uma [MiddlewareException].
|
|
15
|
+
static Response fromMiddleware(MiddlewareException failure) =>
|
|
16
|
+
_json(failure.statusCode, failure.message);
|
|
17
|
+
|
|
18
|
+
static Response _json(int statusCode, String message) {
|
|
19
|
+
return Response.json(
|
|
20
|
+
statusCode: statusCode,
|
|
21
|
+
body: {
|
|
22
|
+
'error': {'code': _codeFor(statusCode), 'message': message},
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static String _codeFor(int statusCode) => switch (statusCode) {
|
|
28
|
+
HttpStatus.badRequest => 'bad_request',
|
|
29
|
+
HttpStatus.unauthorized => 'unauthorized',
|
|
30
|
+
HttpStatus.forbidden => 'forbidden',
|
|
31
|
+
HttpStatus.notFound => 'not_found',
|
|
32
|
+
HttpStatus.methodNotAllowed => 'method_not_allowed',
|
|
33
|
+
_ => statusCode >= 500 ? 'internal_error' : 'error',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Caminho: lib/common/exception/exceptions.dart
|
|
2
|
+
// Camadas de erro fixas do padrão DS9.
|
|
3
|
+
|
|
4
|
+
/// Falha de regra de negócio ou de persistência (services e repositories).
|
|
5
|
+
class BusinessException implements Exception {
|
|
6
|
+
/// Cria uma falha de negócio.
|
|
7
|
+
const BusinessException({
|
|
8
|
+
required this.domainName,
|
|
9
|
+
required this.statusCode,
|
|
10
|
+
required this.message,
|
|
11
|
+
required this.error,
|
|
12
|
+
required this.stackTrace,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/// Contexto de negócio onde a falha ocorreu (ex.: `order`).
|
|
16
|
+
final String domainName;
|
|
17
|
+
|
|
18
|
+
/// Código HTTP associado à falha.
|
|
19
|
+
final int statusCode;
|
|
20
|
+
|
|
21
|
+
/// Mensagem segura para o cliente.
|
|
22
|
+
final String message;
|
|
23
|
+
|
|
24
|
+
/// Causa original; nunca deve ser enviada ao cliente.
|
|
25
|
+
final Object error;
|
|
26
|
+
|
|
27
|
+
/// Stack trace original, usado apenas em log.
|
|
28
|
+
final StackTrace stackTrace;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Falha de entrada ou acesso, exposta pelo controller ao cliente.
|
|
32
|
+
class ClientException implements Exception {
|
|
33
|
+
/// Cria uma falha de cliente.
|
|
34
|
+
const ClientException({required this.statusCode, required this.message});
|
|
35
|
+
|
|
36
|
+
/// Código HTTP da resposta.
|
|
37
|
+
final int statusCode;
|
|
38
|
+
|
|
39
|
+
/// Mensagem segura para o cliente.
|
|
40
|
+
final String message;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Falha ocorrida em um middleware (autenticação, autorização, rate limit).
|
|
44
|
+
class MiddlewareException implements Exception {
|
|
45
|
+
/// Cria uma falha de middleware.
|
|
46
|
+
const MiddlewareException({
|
|
47
|
+
required this.statusCode,
|
|
48
|
+
required this.message,
|
|
49
|
+
this.stackTrace,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/// Código HTTP da resposta.
|
|
53
|
+
final int statusCode;
|
|
54
|
+
|
|
55
|
+
/// Mensagem segura para o cliente.
|
|
56
|
+
final String message;
|
|
57
|
+
|
|
58
|
+
/// Stack trace original, usado apenas em log.
|
|
59
|
+
final StackTrace? stackTrace;
|
|
60
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Caminho: routes/api/orders/_middleware.dart
|
|
2
|
+
// Composição do contexto: ÚNICO lugar onde service/repository/controller são criados.
|
|
3
|
+
import 'package:dart_frog/dart_frog.dart';
|
|
4
|
+
import 'package:my_api/orders/adapter/controllers/order_controller.dart';
|
|
5
|
+
import 'package:my_api/orders/adapter/repository/order_repository.dart';
|
|
6
|
+
import 'package:my_api/orders/domain/ports/order_repository_port.dart';
|
|
7
|
+
import 'package:my_api/orders/domain/services/order_service.dart';
|
|
8
|
+
|
|
9
|
+
OrderRepositoryPort? _repository;
|
|
10
|
+
OrderService? _service;
|
|
11
|
+
OrderController? _controller;
|
|
12
|
+
|
|
13
|
+
/// Provê o controller de pedidos para as rotas deste ramo.
|
|
14
|
+
///
|
|
15
|
+
/// Quem depende vem primeiro: controller → service → repository.
|
|
16
|
+
/// O `OrderDataSource` é provido por um middleware acima (ex.: `routes/_middleware.dart`).
|
|
17
|
+
Handler middleware(Handler handler) {
|
|
18
|
+
return handler
|
|
19
|
+
.use(
|
|
20
|
+
provider<OrderController>(
|
|
21
|
+
(context) => _controller ??= OrderController(context.read<OrderService>()),
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
.use(
|
|
25
|
+
provider<OrderService>(
|
|
26
|
+
(context) => _service ??= OrderService(context.read<OrderRepositoryPort>()),
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
.use(
|
|
30
|
+
provider<OrderRepositoryPort>(
|
|
31
|
+
(context) => _repository ??= OrderRepository(context.read<OrderDataSource>()),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Caminho: lib/orders/domain/models/order.dart
|
|
2
|
+
import 'package:equatable/equatable.dart';
|
|
3
|
+
|
|
4
|
+
/// Pedido do domínio; não conhece HTTP, DTO nem persistência.
|
|
5
|
+
class Order extends Equatable {
|
|
6
|
+
/// Cria um pedido.
|
|
7
|
+
const Order({required this.id, required this.ownerId, required this.total});
|
|
8
|
+
|
|
9
|
+
/// Identificador único do pedido.
|
|
10
|
+
final String id;
|
|
11
|
+
|
|
12
|
+
/// Identificador do usuário dono do pedido.
|
|
13
|
+
final String ownerId;
|
|
14
|
+
|
|
15
|
+
/// Valor total do pedido.
|
|
16
|
+
final double total;
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
List<Object?> get props => [id, ownerId, total];
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Caminho: lib/orders/domain/ports/order_repository_port.dart
|
|
2
|
+
import 'package:fpdart/fpdart.dart';
|
|
3
|
+
|
|
4
|
+
import '../../../common/exception/exceptions.dart';
|
|
5
|
+
import '../models/order.dart';
|
|
6
|
+
|
|
7
|
+
/// Contrato de acesso a pedidos exigido pelo domínio.
|
|
8
|
+
abstract interface class OrderRepositoryPort {
|
|
9
|
+
/// Busca o pedido [id]; o `Right` é `null` quando não existe.
|
|
10
|
+
TaskEither<BusinessException, Order?> findById(String id);
|
|
11
|
+
|
|
12
|
+
/// Persiste um novo pedido do usuário [ownerId] com o valor [total].
|
|
13
|
+
TaskEither<BusinessException, Order> create({
|
|
14
|
+
required String ownerId,
|
|
15
|
+
required double total,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Caminho: lib/orders/adapter/repository/order_repository.dart
|
|
2
|
+
// Agnóstico de persistência: o acesso real fica atrás de [OrderDataSource],
|
|
3
|
+
// implementado conforme a skill de persistência do projeto.
|
|
4
|
+
import 'dart:io';
|
|
5
|
+
|
|
6
|
+
import 'package:fpdart/fpdart.dart';
|
|
7
|
+
|
|
8
|
+
import '../../../common/exception/exceptions.dart';
|
|
9
|
+
import '../../domain/models/order.dart';
|
|
10
|
+
import '../../domain/ports/order_repository_port.dart';
|
|
11
|
+
|
|
12
|
+
/// Fonte de dados de pedidos (adapter de infraestrutura).
|
|
13
|
+
abstract interface class OrderDataSource {
|
|
14
|
+
/// Lê o pedido [id]; retorna `null` se não existir.
|
|
15
|
+
Future<Order?> read(String id);
|
|
16
|
+
|
|
17
|
+
/// Grava um pedido e retorna o registro criado.
|
|
18
|
+
Future<Order> insert({required String ownerId, required double total});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Implementa o port de pedidos sobre uma [OrderDataSource].
|
|
22
|
+
class OrderRepository implements OrderRepositoryPort {
|
|
23
|
+
/// Cria o repository com a [_dataSource] injetada pelo middleware.
|
|
24
|
+
const OrderRepository(this._dataSource);
|
|
25
|
+
|
|
26
|
+
final OrderDataSource _dataSource;
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
TaskEither<BusinessException, Order?> findById(String id) {
|
|
30
|
+
return TaskEither.tryCatch(
|
|
31
|
+
() => _dataSource.read(id),
|
|
32
|
+
(error, stackTrace) => _failure('Falha ao consultar o pedido.', error, stackTrace),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
TaskEither<BusinessException, Order> create({
|
|
38
|
+
required String ownerId,
|
|
39
|
+
required double total,
|
|
40
|
+
}) {
|
|
41
|
+
return TaskEither.tryCatch(
|
|
42
|
+
() => _dataSource.insert(ownerId: ownerId, total: total),
|
|
43
|
+
(error, stackTrace) => _failure('Falha ao criar o pedido.', error, stackTrace),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
BusinessException _failure(String message, Object error, StackTrace stackTrace) {
|
|
48
|
+
return BusinessException(
|
|
49
|
+
domainName: 'order',
|
|
50
|
+
statusCode: HttpStatus.internalServerError,
|
|
51
|
+
message: message,
|
|
52
|
+
error: error,
|
|
53
|
+
stackTrace: stackTrace,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Caminho: routes/api/orders/index.dart -> /api/orders
|
|
2
|
+
// Rotas dentro de routes/ importam via `package:` (fora de lib/ não há import relativo).
|
|
3
|
+
import 'dart:io';
|
|
4
|
+
|
|
5
|
+
import 'package:dart_frog/dart_frog.dart';
|
|
6
|
+
import 'package:my_api/common/http/error_response.dart';
|
|
7
|
+
import 'package:my_api/common/middleware/auth_middleware.dart';
|
|
8
|
+
import 'package:my_api/orders/adapter/controllers/order_controller.dart';
|
|
9
|
+
|
|
10
|
+
/// Despacha o método HTTP; sem regra de negócio nem dependências instanciadas.
|
|
11
|
+
Future<Response> onRequest(RequestContext context) async {
|
|
12
|
+
final controller = context.read<OrderController>();
|
|
13
|
+
final user = context.read<AuthenticatedUser>();
|
|
14
|
+
|
|
15
|
+
switch (context.request.method) {
|
|
16
|
+
case HttpMethod.post:
|
|
17
|
+
final result = await controller.create(context, user.id).run();
|
|
18
|
+
return result.match(
|
|
19
|
+
ErrorResponse.fromClient,
|
|
20
|
+
(order) => Response.json(statusCode: HttpStatus.created, body: order.toJson()),
|
|
21
|
+
);
|
|
22
|
+
case _:
|
|
23
|
+
return Response(statusCode: HttpStatus.methodNotAllowed);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Caminho: routes/api/orders/[id].dart -> /api/orders/:id
|
|
2
|
+
import 'dart:io';
|
|
3
|
+
|
|
4
|
+
import 'package:dart_frog/dart_frog.dart';
|
|
5
|
+
import 'package:my_api/common/http/error_response.dart';
|
|
6
|
+
import 'package:my_api/common/middleware/auth_middleware.dart';
|
|
7
|
+
import 'package:my_api/orders/adapter/controllers/order_controller.dart';
|
|
8
|
+
|
|
9
|
+
/// O parâmetro de caminho chega como argumento posicional [id].
|
|
10
|
+
Future<Response> onRequest(RequestContext context, String id) async {
|
|
11
|
+
final controller = context.read<OrderController>();
|
|
12
|
+
final user = context.read<AuthenticatedUser>();
|
|
13
|
+
|
|
14
|
+
switch (context.request.method) {
|
|
15
|
+
case HttpMethod.get:
|
|
16
|
+
final result = await controller.getById(id, user.id).run();
|
|
17
|
+
return result.match(
|
|
18
|
+
ErrorResponse.fromClient,
|
|
19
|
+
(order) => Response.json(body: order.toJson()),
|
|
20
|
+
);
|
|
21
|
+
case _:
|
|
22
|
+
return Response(statusCode: HttpStatus.methodNotAllowed);
|
|
23
|
+
}
|
|
24
|
+
}
|