@ds9-solutions/mcp 0.1.0 → 0.2.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/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,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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Caminho: test/routes/api/orders/index_test.dart
|
|
2
|
+
// Rota testada como função, com RequestContext mockado; sempre cobre o 405.
|
|
3
|
+
import 'dart:io';
|
|
4
|
+
|
|
5
|
+
import 'package:dart_frog/dart_frog.dart';
|
|
6
|
+
import 'package:mocktail/mocktail.dart';
|
|
7
|
+
import 'package:test/test.dart';
|
|
8
|
+
|
|
9
|
+
import '../../../../routes/api/orders/index.dart' as route;
|
|
10
|
+
import 'package:my_api/common/middleware/auth_middleware.dart';
|
|
11
|
+
import 'package:my_api/orders/adapter/controllers/order_controller.dart';
|
|
12
|
+
|
|
13
|
+
class _MockRequestContext extends Mock implements RequestContext {}
|
|
14
|
+
|
|
15
|
+
class _MockOrderController extends Mock implements OrderController {}
|
|
16
|
+
|
|
17
|
+
void main() {
|
|
18
|
+
late _MockRequestContext context;
|
|
19
|
+
|
|
20
|
+
setUp(() {
|
|
21
|
+
context = _MockRequestContext();
|
|
22
|
+
when(() => context.read<OrderController>()).thenReturn(_MockOrderController());
|
|
23
|
+
when(() => context.read<AuthenticatedUser>()).thenReturn(const AuthenticatedUser(id: 'u1'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('retorna 405 para método não suportado', () async {
|
|
27
|
+
when(() => context.request).thenReturn(Request.delete(Uri.parse('http://localhost/api/orders')));
|
|
28
|
+
|
|
29
|
+
final response = await route.onRequest(context);
|
|
30
|
+
|
|
31
|
+
expect(response.statusCode, equals(HttpStatus.methodNotAllowed));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Caminho: lib/orders/domain/services/order_service.dart
|
|
2
|
+
import 'dart:io';
|
|
3
|
+
|
|
4
|
+
import 'package:fpdart/fpdart.dart';
|
|
5
|
+
|
|
6
|
+
import '../../../common/exception/exceptions.dart';
|
|
7
|
+
import '../models/order.dart';
|
|
8
|
+
import '../ports/order_repository_port.dart';
|
|
9
|
+
|
|
10
|
+
/// Regras de negócio de pedidos.
|
|
11
|
+
class OrderService {
|
|
12
|
+
/// Cria o serviço com o [_repository] injetado pelo middleware.
|
|
13
|
+
const OrderService(this._repository);
|
|
14
|
+
|
|
15
|
+
final OrderRepositoryPort _repository;
|
|
16
|
+
|
|
17
|
+
/// Busca o pedido [id] e garante que pertence a [requesterId].
|
|
18
|
+
TaskEither<BusinessException, Order> getOwnedById({
|
|
19
|
+
required String id,
|
|
20
|
+
required String requesterId,
|
|
21
|
+
}) {
|
|
22
|
+
return _repository.findById(id).flatMap((order) {
|
|
23
|
+
if (order == null) return TaskEither.left(_failure(HttpStatus.notFound, 'Pedido não encontrado.'));
|
|
24
|
+
if (order.ownerId != requesterId) {
|
|
25
|
+
return TaskEither.left(_failure(HttpStatus.forbidden, 'Acesso negado.'));
|
|
26
|
+
}
|
|
27
|
+
return TaskEither.right(order);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Cria um pedido para [ownerId]; rejeita [total] não positivo.
|
|
32
|
+
TaskEither<BusinessException, Order> create({
|
|
33
|
+
required String ownerId,
|
|
34
|
+
required double total,
|
|
35
|
+
}) {
|
|
36
|
+
if (total <= 0) {
|
|
37
|
+
return TaskEither.left(_failure(HttpStatus.badRequest, 'O total deve ser positivo.'));
|
|
38
|
+
}
|
|
39
|
+
return _repository.create(ownerId: ownerId, total: total);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
BusinessException _failure(int statusCode, String message) {
|
|
43
|
+
return BusinessException(
|
|
44
|
+
domainName: 'order',
|
|
45
|
+
statusCode: statusCode,
|
|
46
|
+
message: message,
|
|
47
|
+
error: message,
|
|
48
|
+
stackTrace: StackTrace.current,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Caminho: test/orders/domain/services/order_service_test.dart
|
|
2
|
+
// Service testado com fake do port, sem banco real.
|
|
3
|
+
import 'dart:io';
|
|
4
|
+
|
|
5
|
+
import 'package:fpdart/fpdart.dart';
|
|
6
|
+
import 'package:test/test.dart';
|
|
7
|
+
|
|
8
|
+
import 'package:my_api/common/exception/exceptions.dart';
|
|
9
|
+
import 'package:my_api/orders/domain/models/order.dart';
|
|
10
|
+
import 'package:my_api/orders/domain/ports/order_repository_port.dart';
|
|
11
|
+
import 'package:my_api/orders/domain/services/order_service.dart';
|
|
12
|
+
|
|
13
|
+
class _FakeRepository implements OrderRepositoryPort {
|
|
14
|
+
_FakeRepository(this.stored);
|
|
15
|
+
|
|
16
|
+
final Order? stored;
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
TaskEither<BusinessException, Order?> findById(String id) => TaskEither.right(stored);
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
TaskEither<BusinessException, Order> create({
|
|
23
|
+
required String ownerId,
|
|
24
|
+
required double total,
|
|
25
|
+
}) =>
|
|
26
|
+
TaskEither.right(Order(id: '1', ownerId: ownerId, total: total));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void main() {
|
|
30
|
+
group('OrderService.getOwnedById', () {
|
|
31
|
+
test('retorna Left 404 quando o pedido não existe', () async {
|
|
32
|
+
final service = OrderService(_FakeRepository(null));
|
|
33
|
+
|
|
34
|
+
final result = await service.getOwnedById(id: '1', requesterId: 'u1').run();
|
|
35
|
+
|
|
36
|
+
expect(result.getLeft().toNullable()?.statusCode, HttpStatus.notFound);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('retorna Left 403 quando o pedido é de outro usuário', () async {
|
|
40
|
+
final service = OrderService(_FakeRepository(const Order(id: '1', ownerId: 'u2', total: 5)));
|
|
41
|
+
|
|
42
|
+
final result = await service.getOwnedById(id: '1', requesterId: 'u1').run();
|
|
43
|
+
|
|
44
|
+
expect(result.getLeft().toNullable()?.statusCode, HttpStatus.forbidden);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('retorna Right quando o pedido pertence ao usuário', () async {
|
|
48
|
+
final service = OrderService(_FakeRepository(const Order(id: '1', ownerId: 'u1', total: 5)));
|
|
49
|
+
|
|
50
|
+
final result = await service.getOwnedById(id: '1', requesterId: 'u1').run();
|
|
51
|
+
|
|
52
|
+
expect(result.isRight(), isTrue);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Dart Frog — Checklist de review detalhado
|
|
2
|
+
|
|
3
|
+
## Pré-requisitos
|
|
4
|
+
- [ ] `dart_frog` e `fpdart` presentes no `pubspec.yaml` (senão, o usuário foi informado e autorizou a instalação).
|
|
5
|
+
- [ ] Estrutura existente preservada; padrão aplicado ao código novo.
|
|
6
|
+
|
|
7
|
+
## Inegociáveis
|
|
8
|
+
- [ ] Contratos de port/service/repository/controller retornam `TaskEither`.
|
|
9
|
+
- [ ] Operações que podem lançar usam `TaskEither.tryCatch`.
|
|
10
|
+
- [ ] Toda rota termina o `switch` com `case _` → 405.
|
|
11
|
+
- [ ] Dependências compostas só em `_middleware.dart` (`provider`/`context.read`).
|
|
12
|
+
- [ ] `BusinessException`, `ClientException`, `MiddlewareException` nas camadas certas.
|
|
13
|
+
- [ ] Domínio agnóstico de persistência (só ports).
|
|
14
|
+
|
|
15
|
+
## Camadas
|
|
16
|
+
- [ ] Rota fina; só a rota executa `.run()`.
|
|
17
|
+
- [ ] Controller não acessa persistência nem tem regra de negócio.
|
|
18
|
+
- [ ] Service sem HTTP, `RequestContext` ou SDK.
|
|
19
|
+
- [ ] Repository sem regra de negócio; falhas convertidas em `BusinessException`.
|
|
20
|
+
- [ ] Autorização de recurso no service, não no middleware.
|
|
21
|
+
|
|
22
|
+
## Erros e segurança
|
|
23
|
+
- [ ] Sem stack trace, SQL, `error` original ou mensagem de SDK na resposta.
|
|
24
|
+
- [ ] Formato `{error:{code,message}}` único.
|
|
25
|
+
- [ ] Segredos no ambiente; nada sensível em logs.
|
|
26
|
+
- [ ] Papel/perfil do cliente nunca confiado sem validação no servidor.
|
|
27
|
+
|
|
28
|
+
## Qualidade
|
|
29
|
+
- [ ] Skill `dart` seguida (`///` nas declarações públicas, nomenclatura, OOP).
|
|
30
|
+
- [ ] Arquivos gerados regenerados e não editados à mão.
|
|
31
|
+
- [ ] Testes: service, controller, rota (com 405).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Dart Frog — Erros (referência)
|
|
2
|
+
|
|
3
|
+
Complementa a seção 5 do `SKILL.md`. Exemplos: `examples/exceptions.dart`, `error_response.dart`.
|
|
4
|
+
|
|
5
|
+
## Camadas fixas
|
|
6
|
+
|
|
7
|
+
| Exceção | Quem lança (`Left`) | Campos |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `BusinessException` | service, repository | `domainName`, `statusCode`, `message`, `error`, `stackTrace` |
|
|
10
|
+
| `ClientException` | controller | `statusCode`, `message` |
|
|
11
|
+
| `MiddlewareException` | middleware | `statusCode`, `message`, `stackTrace?` |
|
|
12
|
+
|
|
13
|
+
- `message` é **sempre** segura para o cliente. `error` e `stackTrace` são só para log.
|
|
14
|
+
- Ao converter entre camadas, preserve o `statusCode`. Para `5xx`, a mensagem ao cliente é genérica.
|
|
15
|
+
- Não use `Exception` genérica como falha de `TaskEither`.
|
|
16
|
+
|
|
17
|
+
## Status HTTP
|
|
18
|
+
|
|
19
|
+
| Situação | Status |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Entrada inválida, body malformado | 400 |
|
|
22
|
+
| Não autenticado ou credencial inválida | 401 |
|
|
23
|
+
| Autenticado sem permissão (ex.: recurso de outro usuário) | 403 |
|
|
24
|
+
| Recurso inexistente | 404 |
|
|
25
|
+
| Método não suportado | 405 |
|
|
26
|
+
| Conflito de estado | 409 |
|
|
27
|
+
| Falha interna ou de persistência | 500 |
|
|
28
|
+
| Criação com sucesso | 201 |
|
|
29
|
+
|
|
30
|
+
## Formato único
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "error": { "code": "not_found", "message": "Pedido não encontrado." } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Nunca
|
|
37
|
+
|
|
38
|
+
- Enviar stack trace, SQL, causa original (`error`), caminhos, credenciais ou mensagens de SDK.
|
|
39
|
+
- Devolver `f.error.toString()` ao cliente.
|
|
40
|
+
- Capturar erro e responder sucesso vazio.
|
|
41
|
+
- Logar tokens, documentos, e-mail, telefone ou conteúdo sensível completo.
|
|
42
|
+
|
|
43
|
+
## Onde cada falha nasce
|
|
44
|
+
|
|
45
|
+
- Formato de body/query/params: controller/DTO (`400`).
|
|
46
|
+
- Regra de negócio e autorização de recurso: service (`403`/`404`/`409`).
|
|
47
|
+
- Persistência ou integração: repository (`500`, com `error` original preservado).
|
|
48
|
+
- Autenticação e rate limit: middleware (`401`/`429`).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Dart Frog — `fpdart` e `TaskEither` (referência)
|
|
2
|
+
|
|
3
|
+
Complementa a seção 4 do `SKILL.md`. Exemplos: `examples/repository.dart`, `service.dart`, `controller.dart`.
|
|
4
|
+
|
|
5
|
+
## Pré-requisito: `fpdart` instalado
|
|
6
|
+
|
|
7
|
+
1. Procure `fpdart` em `dependencies` no `pubspec.yaml`.
|
|
8
|
+
2. Se não estiver, **informe que ele é obrigatório para seguir o padrão DS9** e peça autorização para instalar: `dart pub add fpdart`.
|
|
9
|
+
3. Não gere código fora do padrão para contornar a ausência.
|
|
10
|
+
|
|
11
|
+
## Regras
|
|
12
|
+
|
|
13
|
+
- Todo contrato de camada que pode falhar retorna `TaskEither<Falha, T>`: `BusinessException` (port/service/repository), `ClientException` (controller).
|
|
14
|
+
- Operação assíncrona que pode lançar vai dentro de `TaskEither.tryCatch(run, onError)`; `onError` converte a causa (`error`, `stackTrace`) na exceção da camada, sem perder a causa original.
|
|
15
|
+
- Composição com `map`, `flatMap` e `mapLeft`. Regra violada: `TaskEither.left(...)`; sucesso: `TaskEither.right(...)`.
|
|
16
|
+
- **Só a rota executa** (`.run()`), e usa `match` para produzir a `Response`.
|
|
17
|
+
- Funções puras e sem I/O (validações locais, construtores, `toJson`) podem retornar valores comuns, ou `Either` quando houver falha de validação.
|
|
18
|
+
- Proibido: `throw` que escape da camada, `catch (_)` amplo, `Exception` genérica como falha, falha convertida em sucesso vazio.
|
|
19
|
+
|
|
20
|
+
## Padrões
|
|
21
|
+
|
|
22
|
+
```dart
|
|
23
|
+
// 1) Acesso externo (repository)
|
|
24
|
+
TaskEither<BusinessException, Order?> findById(String id) => TaskEither.tryCatch(
|
|
25
|
+
() => _dataSource.read(id),
|
|
26
|
+
(error, stackTrace) => BusinessException(
|
|
27
|
+
domainName: 'order',
|
|
28
|
+
statusCode: HttpStatus.internalServerError,
|
|
29
|
+
message: 'Falha ao consultar o pedido.',
|
|
30
|
+
error: error,
|
|
31
|
+
stackTrace: stackTrace,
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// 2) Regra de negócio (service)
|
|
36
|
+
_repository.findById(id).flatMap(
|
|
37
|
+
(order) => order == null ? TaskEither.left(_notFound(id)) : TaskEither.right(order),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// 3) Troca de camada de erro (controller)
|
|
41
|
+
_service.getOwnedById(id: id, requesterId: userId).mapLeft(_toClient).map(OrderResponse.fromDomain);
|
|
42
|
+
|
|
43
|
+
// 4) Execução (rota)
|
|
44
|
+
final result = await controller.getById(id, user.id).run();
|
|
45
|
+
return result.match(ErrorResponse.fromClient, (order) => Response.json(body: order.toJson()));
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Cuidados
|
|
49
|
+
|
|
50
|
+
- Mantenha as cadeias legíveis: se passar de 3 ou 4 etapas, extraia métodos privados nomeados.
|
|
51
|
+
- Não misture `Future` simples e `TaskEither` no mesmo contrato.
|
|
52
|
+
- Teste `Left` e `Right` explicitamente (`result.isLeft()`, `result.getLeft()`, `result.getRight()`).
|
|
53
|
+
- Esta é a exceção funcional aceita pela skill `dart`: vale só para os contratos entre camadas. O restante continua orientado a objetos.
|