@ds9-solutions/mcp 0.3.0 → 0.5.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/content/agents/code-reviewer.agent.md +20 -0
  3. package/content/agents/db-analyser.agent.md +18 -2
  4. package/content/agents/security-reviewer.agent.md +21 -1
  5. package/content/agents/test-writer.agent.md +17 -8
  6. package/content/skills/dart/SKILL.md +19 -0
  7. package/content/skills/dart-frog/SKILL.md +22 -1
  8. package/content/skills/debugger/SKILL.md +144 -0
  9. package/content/skills/debugger/references/red-flags.md +32 -0
  10. package/content/skills/debugger/references/report-format.md +65 -0
  11. package/content/skills/debugger/references/root-cause-tracing.md +42 -0
  12. package/content/skills/debugger/references/safe-diagnostics.md +47 -0
  13. package/content/skills/driftorm/SKILL.md +59 -225
  14. package/content/skills/driftorm/examples/connection_postgres.dart +25 -0
  15. package/content/skills/driftorm/examples/connection_sqlite.dart +13 -0
  16. package/content/skills/driftorm/examples/database.dart +25 -0
  17. package/content/skills/driftorm/examples/migration_manual.dart +24 -0
  18. package/content/skills/driftorm/examples/migration_step_by_step.dart +24 -0
  19. package/content/skills/driftorm/examples/pagination.dart +48 -0
  20. package/content/skills/driftorm/examples/queries.dart +34 -0
  21. package/content/skills/driftorm/examples/repository_hex.dart +63 -0
  22. package/content/skills/driftorm/examples/table.dart +25 -0
  23. package/content/skills/driftorm/examples/test.dart +33 -0
  24. package/content/skills/driftorm/postgres/connection.md +9 -0
  25. package/content/skills/driftorm/postgres/migrations.md +11 -0
  26. package/content/skills/driftorm/postgres/types-and-defaults.md +10 -0
  27. package/content/skills/driftorm/references/anti-patterns.md +20 -0
  28. package/content/skills/driftorm/references/checklist.md +17 -0
  29. package/content/skills/driftorm/references/database-and-connection.md +22 -0
  30. package/content/skills/driftorm/references/dialects.md +17 -0
  31. package/content/skills/driftorm/references/drift-files.md +20 -0
  32. package/content/skills/driftorm/references/migrations.md +26 -0
  33. package/content/skills/driftorm/references/performance.md +10 -0
  34. package/content/skills/driftorm/references/queries-and-pagination.md +46 -0
  35. package/content/skills/driftorm/references/repositories-taskeither.md +29 -0
  36. package/content/skills/driftorm/references/setup.md +37 -0
  37. package/content/skills/driftorm/references/tables-and-columns.md +43 -0
  38. package/content/skills/driftorm/references/testing.md +12 -0
  39. package/content/skills/driftorm/references/transactions.md +23 -0
  40. package/content/skills/driftorm/sqlite/connection.md +11 -0
  41. package/content/skills/driftorm/sqlite/types-and-pragmas.md +9 -0
  42. package/content/skills/flutter/SKILL.md +19 -0
  43. package/content/skills/hex-pattern-dart/SKILL.md +21 -0
  44. package/content/skills/hex-pattern-dart/examples/adapter-in.md +4 -5
  45. package/content/skills/hex-pattern-dart/references/persistence.md +2 -2
  46. package/package.json +1 -1
  47. package/content/skills/docker/SKILL.md +0 -104
  48. package/content/skills/mysql/SKILL.md +0 -94
  49. package/content/skills/typescript/SKILL.md +0 -75
@@ -1,249 +1,83 @@
1
1
  ---
2
2
  name: driftorm
3
- description: Convenções e boas práticas do Drift, a biblioteca de banco de dados type-safe para Dart e Flutter (SQLite e Postgres). Use ao criar, editar, revisar ou testar tabelas, consultas, DAOs, migrations ou a classe de banco em projetos que dependem de drift.
3
+ description: Convenções DS9 para persistência com Drift (banco type-safe para Dart e Flutter) em SQLite e Postgres — classe de banco e conexão, tabelas, queries, paginação, DAOs/repositórios com TaskEither, transações, migrations e testes. Preserva a estrutura já existente no projeto. Use ao criar, editar, revisar ou testar tabelas, queries, DAOs, repositórios, migrations ou a classe de banco em projetos que dependem de `drift`, ou quando pedirem "banco com Drift", "migration Drift" ou "persistência" em Dart/Flutter.
4
4
  ---
5
5
 
6
- # Drift best practices
6
+ # Drift (persistência) — padrão DS9
7
7
 
8
- Baseado na documentação oficial: https://drift.simonbinder.eu (repositório simolus3/drift, `drift` 2.35.x).
8
+ Skill agnóstica de projeto: vale para APIs/back-ends Dart e para apps Flutter, em **SQLite** e **Postgres**. Fonte oficial: https://drift.simonbinder.eu. Confirme a versão do `drift` no `pubspec.yaml` do projeto antes de assumir APIs.
9
9
 
10
- ## Princípio central
10
+ ## Carregamento sob demanda
11
11
 
12
- O Drift gera código a partir do schema que você declara. O schema é a fonte da verdade: tabelas em Dart (ou SQL em arquivos `.drift`), uma classe de banco central e o `build_runner` gerando o resto. Toda mudança de schema é uma migration versionada e testada.
12
+ Este arquivo traz só as regras inegociáveis e o roteamento. Use `list_skill_files` com `driftorm` e leia **apenas** o que a tarefa pedir com `get_skill_file`.
13
13
 
14
- ## Setup
14
+ ## Estrutura existente vence
15
15
 
16
- | Alvo | Dependências | Dev dependencies |
17
- |---|---|---|
18
- | Flutter | `drift`, `drift_flutter`, `path_provider` | `drift_dev`, `build_runner` |
19
- | Dart (SQLite) | `drift`, `sqlite3` | `drift_dev`, `build_runner` |
20
- | Dart (Postgres) | `drift`, `postgres`, `drift_postgres` | `drift_dev`, `build_runner` |
16
+ Se o projeto **já tem** classe de banco, tabelas, migrations ou repositórios, **mantenha a estrutura e o estilo dele** (pastas, nomes, migrations manuais, singleton etc.) e aplique as demais regras desta skill, **principalmente as de segurança**. O que esta skill descreve como padrão vale para código novo e projetos novos. Não refatore estrutura existente sem pedido explícito.
21
17
 
22
- - `dart run build_runner build` gera o código uma vez. Durante o desenvolvimento, use `dart run build_runner watch`.
23
- - Para Postgres, configure `sql: dialects: [postgres]` no `build.yaml`. Por padrão o gerador só emite SQL de SQLite.
18
+ ## Regras inegociáveis
24
19
 
25
- ## Regras
20
+ 1. **`TaskEither` em toda camada.** Repository, service e controller sempre retornam `TaskEither<Falha, T>` (`fpdart`), e o acesso ao banco é envolvido em `TaskEither.tryCatch`. Se `fpdart` não estiver instalado, avise que precisa estar para seguir o padrão DS9. Detalhes: `references/repositories-taskeither.md`.
21
+ 2. **A classe de banco nunca sai do repository.** Handlers, controllers e UI não a acessam; recebem repositórios/DAOs por injeção (middleware, construtor ou o mecanismo de DI do projeto).
22
+ 3. **Uma instância por arquivo/banco**, injetada. Abrir a mesma classe várias vezes sobre o mesmo arquivo quebra streams.
23
+ 4. **Segurança:** valores do usuário sempre como parâmetro (API tipada, `Variable`, `?`/`$1`); nunca concatene entrada em SQL. Credenciais vêm de ambiente/segredos, nunca do código. Mensagens ao cliente não expõem SQL nem a causa original.
24
+ 5. **Schema muda por migration versionada** e testada; incremente `schemaVersion`. O Drift não altera bancos existentes sozinho.
25
+ 6. **Rode o `build_runner`** após mudar tabelas, DAOs ou o annotation do banco.
26
+ 7. **Faça `await` em tudo dentro de `transaction`**; operações atômicas ficam em transação.
27
+ 8. **Paginação no padrão DS9:** `page`/`pageSize`, offset `(page - 1) * pageSize`, resposta com `items`, `total`, `page`, `pageSize`. Veja `references/queries-and-pagination.md`.
28
+ 9. **Defaults:** use `clientDefault` por padrão; use default no banco (`withDefault`) quando ele for parte legítima do schema (ex.: `gen_random_uuid()` no Postgres). Veja `references/tables-and-columns.md`.
29
+ 10. **Documente tudo que for público** com `///` (regra da skill `dart`).
26
30
 
27
- ### Classe de banco
31
+ ## Roteamento: pergunta → arquivo
28
32
 
29
- 1. **Uma classe `@DriftDatabase(tables: [...], daos: [...])`** que estende `_$NomeDatabase`, com `part 'arquivo.g.dart';`.
30
- 2. **O construtor aceita um `QueryExecutor` opcional**: `AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());`. Isso permite injetar um banco em memória nos testes.
31
- 3. **Use uma única instância (singleton) por arquivo de banco.** Abrir a mesma classe várias vezes sobre o mesmo arquivo quebra os streams e gera aviso em runtime. Forneça a instância por DI (Riverpod, get_it, provider).
32
- 4. **Abra a conexão fora da thread principal.** No Flutter, use `driftDatabase(name: ...)`, do `drift_flutter`. Em Dart puro, use `NativeDatabase.createInBackground(File(...))`. Ambos rodam o SQLite em isolate.
33
- 5. **Feche com `await db.close()`** no fim do ciclo de vida (testes, shutdown do servidor).
33
+ | Preciso de… | Arquivo |
34
+ |---|---|
35
+ | Dependências, `build.yaml`, geração | `references/setup.md` |
36
+ | Classe de banco, conexão, DI, fechar | `references/database-and-connection.md` |
37
+ | Tabelas, colunas, defaults, FKs, índices, enums | `references/tables-and-columns.md` |
38
+ | Select/insert/update/delete, joins, paginação, `watch` | `references/queries-and-pagination.md` |
39
+ | Repository/DAO com `TaskEither` e port hexagonal | `references/repositories-taskeither.md` |
40
+ | Transações | `references/transactions.md` |
41
+ | Migrations (novas e em projetos existentes) | `references/migrations.md` |
42
+ | Diferenças SQLite × Postgres | `references/dialects.md` |
43
+ | Detalhes de Postgres | `postgres/connection.md`, `postgres/types-and-defaults.md`, `postgres/migrations.md` |
44
+ | Detalhes de SQLite | `sqlite/connection.md`, `sqlite/types-and-pragmas.md` |
45
+ | SQL em arquivos `.drift` (opcional) | `references/drift-files.md` |
46
+ | Testes | `references/testing.md` |
47
+ | Performance | `references/performance.md` |
48
+ | Erros comuns | `references/anti-patterns.md` |
49
+ | Revisão final | `references/checklist.md` |
34
50
 
35
- ### Tabelas
51
+ Exemplos (Dart, lazy): `examples/table.dart`, `examples/database.dart`, `examples/queries.dart`, `examples/repository_hex.dart`, `examples/pagination.dart`, `examples/migration_step_by_step.dart`, `examples/migration_manual.dart`, `examples/test.dart`, `examples/connection_postgres.dart`, `examples/connection_sqlite.dart`.
36
52
 
37
- 6. **Uma tabela é uma classe que estende `Table`**, com colunas declaradas como `late final campo = tipo()();`. Os parênteses finais são obrigatórios.
38
- 7. **Colunas são não-nulas por padrão.** Use `.nullable()` só quando `null` tiver significado de domínio.
39
- 8. **Chave primária:** `integer().autoIncrement()()` para o caso comum. Para outros casos, sobrescreva `Set<Column> get primaryKey => {...}` (só com `=>` e literal de set).
40
- 9. **Prefira `clientDefault()` a `withDefault()`.** O `clientDefault` é calculado em Dart e não exige migration. O `withDefault` altera o schema.
41
- 10. **Chaves estrangeiras com `.references(Tabela, #coluna, onDelete: ...)`.** No SQLite elas vêm desligadas, então ative com `PRAGMA foreign_keys = ON` no `beforeOpen`.
42
- 11. **Garanta integridade no schema:** `.unique()`, `uniqueKeys` para unicidade composta, `.check(expr)` para validação. Prefira `check` a `withLength`, que só valida em Dart.
43
- 12. **Crie índices com `@TableIndex(name: ..., columns: {#col})`** para colunas muito filtradas. Chave primária, `unique` e alvo de FK já têm índice.
44
- 13. **Em projeto novo, armazene `DateTime` como texto ISO-8601** com `store_date_time_values_as_text: true` no `build.yaml`. O padrão é timestamp Unix em segundos, sem fuso e sem frações.
45
- 14. **Reaproveite colunas comuns** (`id`, `createdAt`) com mixins de tabela.
46
- 15. **Enums e tipos próprios** usam `intEnum<T>()`, `textEnum<T>()` ou `TypeConverter`, em vez de conversão manual espalhada pelo código.
47
- 16. **Rode o gerador de novo** sempre que mudar tabelas, DAOs ou o annotation do banco.
53
+ ## Dialeto
48
54
 
49
- ### Consultas e escrita
55
+ Descubra o dialeto pelo `pubspec.yaml`: `drift_postgres`/`postgres` → Postgres; `sqlite3`/`drift_flutter` → SQLite. Leia a pasta do dialeto correspondente. Em dúvida, pergunte.
50
56
 
51
- 17. **Insira com `Companion.insert(...)`**, que torna obrigatórios só os campos sem default. Para atualização parcial, use `Companion(campo: Value(x))`.
52
- 18. **Use a API tipada** (`select`, `into`, `update`, `delete`, joins, manager) antes de SQL cru. Quando precisar de SQL, prefira arquivos `.drift`, que o gerador valida em tempo de build. `customSelect` fica como último recurso e precisa de `readsFrom` para funcionar com `watch`.
53
- 19. **`get()` para leitura única e `watch()` para UI reativa.** O stream já emite o valor atual ao ser escutado, então não combine `get()` com `watch()`.
54
- 20. **`getSingle()` lança erro se não houver exatamente uma linha.** Quando a ausência é um caso válido, use `getSingleOrNull()` ou `watchSingleOrNull()`.
57
+ ## Relação com outras skills
55
58
 
56
- ### Organização
59
+ - `dart`: estilo e tipagem (a exceção `TaskEither` está lá).
60
+ - `hex-pattern-dart` / `dart-frog`: o port é do domínio; o repository com Drift o implementa.
61
+ - `flutter`: uso do Drift em apps e testes de widget.
62
+ - Agente `db-analyser`: auditoria de schema/migrations; `security-reviewer`: segurança.
57
63
 
58
- 21. **Separe as consultas por domínio em DAOs:** `@DriftAccessor(tables: [...])` estendendo `DatabaseAccessor<AppDatabase>` com o mixin gerado. Registre os DAOs em `daos:` no `@DriftDatabase`.
59
- 22. **Não exponha a classe de banco para a UI ou para os handlers.** Exponha DAOs ou repositórios que retornam modelos e streams.
64
+ ## Registro de atividade (obrigatório)
60
65
 
61
- ### Transações
66
+ Siga o **padrão de registro DS9** (o mesmo da skill `debugger`): ao aplicar esta skill em uma tarefa (banco, tabelas, queries, repositories, migrations), **registre na conversa, em tempo real, tudo o que consome contexto ou tokens**, em uma linha curta por item, antes ou logo depois da ação. O registro é só texto na resposta: **não grave arquivo de log**.
62
67
 
63
- 23. **Operações que precisam ser atômicas vão em `transaction(() async { ... })`.**
64
- 24. **Faça `await` em todas as chamadas dentro da transação.** Query sem `await` pode rodar depois do fechamento e causar perda de dados ou crash. Não agende timers que usem o banco dentro da transação.
65
- 25. **Streams criados fora da transação só são notificados no commit.** Não escute fora da transação um stream criado dentro dela.
68
+ | Tipo | Registre |
69
+ |---|---|
70
+ | Arquivo lido | caminho e trecho (`src/a.ts:10-60`) ou "inteiro" |
71
+ | Skill/reference | `skill:<id>`, `get_skill_file <id> references/x.md` |
72
+ | Busca | padrão e pasta (`grep "X" src/`) |
73
+ | Comando | o comando e o tamanho aproximado da saída (curta/média/longa); saída longa é filtrada ou resumida |
74
+ | Web/MCP | URL ou tool chamada (`Referência externa: <nome/link>` no fallback web) |
75
+ | Subagente | nome e objetivo |
76
+ | Log/stack colado | origem e tamanho aproximado |
77
+ | Arquivo criado/alterado | caminho e motivo (`✏️ Criando: <caminho>` / `✏️ Atualizando: <caminho>`); só quando a alteração for permitida |
66
78
 
67
- ### Migrations
79
+ Formato: `📄 leitura · 🧩 skill · 🔎 busca · ⚙️ comando · 🌐 web/MCP · 🤖 subagente · ✏️ escrita`, por exemplo `📄 lib/orders/order_service.dart:30-90` ou `🧩 get_skill_file <id> references/x.md`.
68
80
 
69
- 26. **Toda mudança de schema exige incrementar o `schemaVersion` e escrever uma migration.** O Drift não altera bancos existentes sozinho.
70
- 27. **Use o fluxo guiado `make-migrations`.** Registre o banco em `databases:` no `build.yaml` e rode `dart run drift_dev make-migrations` **antes** da primeira mudança, para salvar o schema inicial. Depois, a cada mudança, incremente a versão e rode o comando de novo.
71
- 28. **Escreva as migrations com `stepByStep(from1To2: ..., from2To3: ...)`**, usando o `schema` recebido e nunca as tabelas atuais da classe. Extraia o `stepByStep` para um getter de extensão em `GeneratedDatabase`, para não referenciar o schema atual por acidente.
72
- 29. **Rode os testes de migration gerados** (em `test/drift/`) e versione a pasta `drift_schemas/` no Git.
73
- 30. **Evite consultas dentro de callbacks de migration.** Se precisar, rode-as só depois de o schema chegar ao estado que elas esperam.
74
- 31. **Pragmas e seed vão no `beforeOpen`**, que roda a cada abertura. Use `details.wasCreated` e `details.hadUpgrade` para condicionar o seed.
81
+ Registre também cada arquivo de `references/`, `examples/`, `postgres/` ou `sqlite/` consultado e o dialeto detectado.
75
82
 
76
- ### Testes
77
-
78
- 32. **Teste com `NativeDatabase.memory()`**, abrindo no `setUp` e fechando no `tearDown`. Em widget tests do Flutter, envolva com `DatabaseConnection(..., closeStreamsSynchronously: true)`.
79
-
80
- ## Exemplos
81
-
82
- ### Tabelas, banco e conexão (Flutter)
83
-
84
- ```dart
85
- // lib/database/database.dart
86
- import 'package:drift/drift.dart';
87
- import 'package:drift_flutter/drift_flutter.dart';
88
- import 'package:path_provider/path_provider.dart';
89
-
90
- import 'database.steps.dart';
91
-
92
- part 'database.g.dart';
93
-
94
- mixin AutoIncrementingPrimaryKey on Table {
95
- late final id = integer().autoIncrement()();
96
- }
97
-
98
- class Categories extends Table with AutoIncrementingPrimaryKey {
99
- late final name = text().unique()();
100
- }
101
-
102
- @TableIndex(name: 'todo_category', columns: {#category})
103
- class TodoItems extends Table with AutoIncrementingPrimaryKey {
104
- // Coluna que se referencia no check precisa de tipo explícito.
105
- late final Column<String> title =
106
- text().check(title.length.isBiggerThanValue(0))();
107
- late final done = boolean().clientDefault(() => false)();
108
- late final category = integer()
109
- .nullable()
110
- .references(Categories, #id, onDelete: KeyAction.setNull)();
111
- late final createdAt = dateTime().clientDefault(DateTime.now)();
112
- }
113
-
114
- @DriftDatabase(tables: [Categories, TodoItems], daos: [TodosDao])
115
- class AppDatabase extends _$AppDatabase {
116
- AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
117
-
118
- @override
119
- int get schemaVersion => 2;
120
-
121
- @override
122
- MigrationStrategy get migration => MigrationStrategy(
123
- onUpgrade: _schemaUpgrade,
124
- beforeOpen: (details) async {
125
- await customStatement('PRAGMA foreign_keys = ON');
126
- },
127
- );
128
-
129
- static QueryExecutor _openConnection() {
130
- return driftDatabase(
131
- name: 'app_database',
132
- native: const DriftNativeOptions(
133
- databaseDirectory: getApplicationSupportDirectory,
134
- ),
135
- );
136
- }
137
- }
138
-
139
- extension Migrations on GeneratedDatabase {
140
- OnUpgrade get _schemaUpgrade => stepByStep(
141
- from1To2: (m, schema) async {
142
- await m.createTable(schema.categories);
143
- await m.addColumn(schema.todoItems, schema.todoItems.category);
144
- },
145
- );
146
- }
147
- ```
148
-
149
- ### DAO com stream e transação
150
-
151
- ```dart
152
- // lib/database/todos_dao.dart
153
- import 'package:drift/drift.dart';
154
-
155
- import 'database.dart';
156
-
157
- part 'todos_dao.g.dart';
158
-
159
- @DriftAccessor(tables: [TodoItems, Categories])
160
- class TodosDao extends DatabaseAccessor<AppDatabase> with _$TodosDaoMixin {
161
- TodosDao(super.attachedDatabase);
162
-
163
- Stream<List<TodoItem>> watchPending() {
164
- return (select(todoItems)
165
- ..where((t) => t.done.equals(false))
166
- ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
167
- .watch();
168
- }
169
-
170
- Future<int> add(String title) {
171
- return into(todoItems).insert(TodoItemsCompanion.insert(title: title));
172
- }
173
-
174
- Future<void> deleteCategory(int categoryId) {
175
- return transaction(() async {
176
- await (update(todoItems)..where((t) => t.category.equals(categoryId)))
177
- .write(const TodoItemsCompanion(category: Value(null)));
178
- await (delete(categories)..where((c) => c.id.equals(categoryId))).go();
179
- });
180
- }
181
- }
182
- ```
183
-
184
- ### Conexão Postgres (backend Dart)
185
-
186
- ```dart
187
- import 'dart:io';
188
-
189
- import 'package:drift_postgres/drift_postgres.dart';
190
- import 'package:postgres/postgres.dart' as pg;
191
-
192
- QueryExecutor openPostgres() {
193
- final env = Platform.environment;
194
- return PgDatabase(
195
- endpoint: pg.Endpoint(
196
- host: env['DB_HOST']!,
197
- database: env['DB_NAME']!,
198
- username: env['DB_USER'],
199
- password: env['DB_PASSWORD'],
200
- ),
201
- );
202
- }
203
- ```
204
-
205
- ### Teste
206
-
207
- ```dart
208
- import 'package:drift/drift.dart';
209
- import 'package:drift/native.dart';
210
- import 'package:test/test.dart';
211
-
212
- import 'package:my_app/database/database.dart';
213
-
214
- void main() {
215
- late AppDatabase db;
216
-
217
- setUp(() {
218
- db = AppDatabase(
219
- DatabaseConnection(
220
- NativeDatabase.memory(),
221
- closeStreamsSynchronously: true,
222
- ),
223
- );
224
- });
225
-
226
- tearDown(() => db.close());
227
-
228
- test('watchPending emite o item recém-criado', () async {
229
- final stream = db.todosDao.watchPending();
230
- await db.todosDao.add('Escrever testes');
231
-
232
- final items = await stream.first;
233
- expect(items.map((t) => t.title), contains('Escrever testes'));
234
- });
235
- }
236
- ```
237
-
238
- ## Checklist de revisão
239
-
240
- - [ ] Mudou tabela, DAO ou annotation? O `build_runner` foi rodado de novo?
241
- - [ ] Mudou o schema? O `schemaVersion` foi incrementado, o `make-migrations` foi rodado e existe um passo `fromXToY` testado?
242
- - [ ] A migration usa `schema.*` e não as tabelas atuais da classe?
243
- - [ ] Existe uma única instância do banco, injetada por DI?
244
- - [ ] A conexão usa `driftDatabase` ou `createInBackground`?
245
- - [ ] FKs no SQLite têm `PRAGMA foreign_keys = ON`?
246
- - [ ] Toda chamada dentro de `transaction` tem `await`?
247
- - [ ] Defaults usam `clientDefault`, salvo quando o default precisa estar no schema?
248
- - [ ] `getSingle` é usado só onde a linha certamente existe?
249
- - [ ] Os testes usam `NativeDatabase.memory()` e fecham o banco no `tearDown`?
83
+ Regras: leia o mínimo necessário (trechos em vez de arquivos inteiros, filtros em saídas de comando), não releia o que já leu e mascare segredos no registro. Ao final, inclua o resumo **"Consumo da tarefa"**: tabela com tipo, itens e observação, apontando o que foi mais caro e como reduzir.
@@ -0,0 +1,25 @@
1
+ // Caminho: lib/common/infra/database/connection_postgres.dart
2
+ import 'dart:io';
3
+
4
+ import 'package:drift/drift.dart';
5
+ import 'package:drift_postgres/drift_postgres.dart';
6
+ import 'package:postgres/postgres.dart' as pg;
7
+
8
+ /// Abre a conexão Postgres a partir de variáveis de ambiente.
9
+ QueryExecutor openPostgres() {
10
+ final env = Platform.environment;
11
+ final isProduction = env['APP_ENV'] == 'production';
12
+ return PgDatabase(
13
+ endpoint: pg.Endpoint(
14
+ host: env['DB_HOST']!,
15
+ port: int.tryParse(env['DB_PORT'] ?? '') ?? 5432,
16
+ database: env['DB_NAME']!,
17
+ username: env['DB_USER'],
18
+ password: env['DB_PASSWORD'],
19
+ ),
20
+ // TLS obrigatório em produção; desligado só em desenvolvimento local.
21
+ settings: pg.ConnectionSettings(
22
+ sslMode: isProduction ? pg.SslMode.require : pg.SslMode.disable,
23
+ ),
24
+ );
25
+ }
@@ -0,0 +1,13 @@
1
+ // Caminho: lib/common/infra/database/connection_sqlite.dart
2
+ import 'dart:io';
3
+
4
+ import 'package:drift/drift.dart';
5
+ import 'package:drift/native.dart';
6
+
7
+ /// Abre um SQLite em isolate (Dart puro). No Flutter, use `driftDatabase(...)`.
8
+ QueryExecutor openSqlite(String path) {
9
+ return NativeDatabase.createInBackground(File(path));
10
+ }
11
+
12
+ /// Banco em memória para testes.
13
+ QueryExecutor openInMemory() => NativeDatabase.memory();
@@ -0,0 +1,25 @@
1
+ // Caminho: lib/common/infra/database/database.dart
2
+ import 'package:drift/drift.dart';
3
+
4
+ import 'tables/order_table.dart';
5
+
6
+ part 'database.g.dart';
7
+
8
+ /// Banco da aplicação. Instância única, injetada na raiz de composição.
9
+ @DriftDatabase(tables: [OrderTable])
10
+ class AppDatabase extends _$AppDatabase {
11
+ /// Cria o banco; [executor] permite injetar um banco em memória nos testes.
12
+ AppDatabase(QueryExecutor executor) : super(executor);
13
+
14
+ @override
15
+ int get schemaVersion => 1;
16
+
17
+ @override
18
+ MigrationStrategy get migration => MigrationStrategy(
19
+ onCreate: (m) => m.createAll(),
20
+ beforeOpen: (details) async {
21
+ // SQLite: ligue as FKs. Em Postgres, remova esta linha.
22
+ await customStatement('PRAGMA foreign_keys = ON');
23
+ },
24
+ );
25
+ }
@@ -0,0 +1,24 @@
1
+ // Migration manual (estilo comum em projetos existentes; mantenha-o se já usado).
2
+ // DDL idempotente e SQL fixo, sem entrada externa.
3
+ import 'package:drift/drift.dart';
4
+
5
+ MigrationStrategy buildMigration(GeneratedDatabase db) {
6
+ return MigrationStrategy(
7
+ onCreate: (m) => m.createAll(),
8
+ onUpgrade: (m, from, to) async {
9
+ if (from < 2) {
10
+ await db.customStatement(
11
+ 'ALTER TABLE order_table '
12
+ 'ADD COLUMN IF NOT EXISTS deleted BOOLEAN NOT NULL DEFAULT FALSE',
13
+ );
14
+ }
15
+ if (from < 3) {
16
+ await db.customStatement(
17
+ 'CREATE INDEX IF NOT EXISTS order_owner_idx '
18
+ 'ON order_table (owner_id)',
19
+ );
20
+ }
21
+ },
22
+ );
23
+ }
24
+ // Lembre de incrementar `schemaVersion` junto com cada bloco `from < n`.
@@ -0,0 +1,24 @@
1
+ // Migration guiada (padrão DS9 para projetos novos).
2
+ // Pré-requisito: `databases:` no build.yaml e `dart run drift_dev make-migrations`.
3
+ import 'package:drift/drift.dart';
4
+
5
+ import 'database.steps.dart';
6
+
7
+ extension Migrations on GeneratedDatabase {
8
+ /// Usa `schema.*` (snapshot da versão), nunca as tabelas atuais da classe.
9
+ OnUpgrade get schemaUpgrade => stepByStep(
10
+ from1To2: (m, schema) async {
11
+ await m.addColumn(schema.orderTable, schema.orderTable.deleted);
12
+ },
13
+ from2To3: (m, schema) async {
14
+ await m.createIndex(schema.orderOwnerIdx);
15
+ },
16
+ );
17
+ }
18
+
19
+ // Na classe de banco:
20
+ // @override
21
+ // MigrationStrategy get migration => MigrationStrategy(
22
+ // onCreate: (m) => m.createAll(),
23
+ // onUpgrade: schemaUpgrade,
24
+ // );
@@ -0,0 +1,48 @@
1
+ // Caminho: lib/orders/adapter/repository/order_repository.dart (trecho)
2
+ // Paginação no padrão DS9: page/pageSize, offset (page - 1) * pageSize,
3
+ // resposta com items/total/page/pageSize.
4
+ import 'package:drift/drift.dart';
5
+ import 'package:fpdart/fpdart.dart';
6
+
7
+ TaskEither<BusinessException, PaginateResponse<Order>> listByOwner({
8
+ required String ownerId,
9
+ required PaginationRequest pagination,
10
+ }) {
11
+ return TaskEither.tryCatch(
12
+ () async {
13
+ final page = pagination.page;
14
+ final pageSize = pagination.pageSize;
15
+ final offset = (page - 1) * pageSize;
16
+
17
+ final filter = _db.orderTable.ownerId.equals(ownerId) &
18
+ _db.orderTable.deleted.equals(false);
19
+
20
+ // Total sobre os mesmos filtros, sem limit.
21
+ final count = _db.orderTable.id.count();
22
+ final total = await (_db.selectOnly(_db.orderTable)
23
+ ..addColumns([count])
24
+ ..where(filter))
25
+ .map((r) => r.read(count) ?? 0)
26
+ .getSingle();
27
+
28
+ final rows = await (_db.select(_db.orderTable)
29
+ ..where((t) => filter)
30
+ ..orderBy([
31
+ (t) => OrderingTerm.desc(t.createdAt),
32
+ (t) => OrderingTerm.asc(t.id), // desempate estável
33
+ ])
34
+ ..limit(pageSize, offset: offset))
35
+ .get();
36
+
37
+ return PaginateResponse(
38
+ items: rows.map(_toModel).toList(),
39
+ total: total,
40
+ page: page,
41
+ pageSize: pageSize,
42
+ );
43
+ },
44
+ (e, s) => _failure('Falha ao listar pedidos.', e, s),
45
+ );
46
+ }
47
+ // Projeto existente que calcula o total com `base.get().then((r) => r.length)`:
48
+ // mantenha o que ele já usa.
@@ -0,0 +1,34 @@
1
+ // Trecho: queries comuns dentro de um repository (cada uma em tryCatch).
2
+ import 'package:drift/drift.dart';
3
+
4
+ // Update parcial.
5
+ Future<int> markDeleted(String id) {
6
+ return (_db.update(_db.orderTable)..where((t) => t.id.equals(id))).write(
7
+ OrderTableCompanion(
8
+ deleted: const Value(true),
9
+ ),
10
+ );
11
+ }
12
+
13
+ // Join tipado.
14
+ Future<List<(OrderTableData, ItemTableData)>> withItems(String ownerId) async {
15
+ final query = _db.select(_db.orderTable).join([
16
+ innerJoin(_db.itemTable, _db.itemTable.orderId.equalsExp(_db.orderTable.id)),
17
+ ])
18
+ ..where(_db.orderTable.ownerId.equals(ownerId));
19
+ final rows = await query.get();
20
+ return rows
21
+ .map((r) => (r.readTable(_db.orderTable), r.readTable(_db.itemTable)))
22
+ .toList();
23
+ }
24
+
25
+ // SQL cru só quando necessário: sempre parametrizado.
26
+ Future<int> incrementCounter(String key) async {
27
+ final row = await _db.customSelect(
28
+ r'INSERT INTO rate_limit_table ("key", count) VALUES ($1, 1) '
29
+ r'ON CONFLICT ("key") DO UPDATE SET count = rate_limit_table.count + 1 '
30
+ 'RETURNING count',
31
+ variables: [Variable.withString(key)],
32
+ ).getSingle();
33
+ return row.read<int>('count');
34
+ }
@@ -0,0 +1,63 @@
1
+ // Caminho: lib/orders/adapter/repository/order_repository.dart
2
+ // Implementa o port do domínio (hex-pattern-dart / dart-frog) com Drift.
3
+ // Toda operação retorna TaskEither e o acesso ao banco fica em tryCatch.
4
+ import 'dart:io';
5
+
6
+ import 'package:drift/drift.dart';
7
+ import 'package:fpdart/fpdart.dart';
8
+
9
+ import '../../../common/exception/exceptions.dart';
10
+ import '../../../common/infra/database/database.dart';
11
+ import '../../domain/models/order.dart';
12
+ import '../../domain/ports/order_repository_port.dart';
13
+
14
+ /// Repository de pedidos sobre Drift.
15
+ class OrderRepository implements OrderRepositoryPort {
16
+ /// Cria o repository com o [_db] injetado pela raiz de composição.
17
+ const OrderRepository(this._db);
18
+
19
+ final AppDatabase _db;
20
+
21
+ @override
22
+ TaskEither<BusinessException, Order?> findById(String id) {
23
+ return TaskEither.tryCatch(
24
+ () async {
25
+ final row = await (_db.select(_db.orderTable)
26
+ ..where((t) => t.id.equals(id) & t.deleted.equals(false)))
27
+ .getSingleOrNull();
28
+ return row == null ? null : _toModel(row);
29
+ },
30
+ (e, s) => _failure('Falha ao consultar o pedido.', e, s),
31
+ );
32
+ }
33
+
34
+ @override
35
+ TaskEither<BusinessException, Order> create({
36
+ required String ownerId,
37
+ required double total,
38
+ }) {
39
+ return TaskEither.tryCatch(
40
+ () async {
41
+ final row = await _db.into(_db.orderTable).insertReturning(
42
+ OrderTableCompanion.insert(ownerId: ownerId, total: total),
43
+ );
44
+ return _toModel(row);
45
+ },
46
+ (e, s) => _failure('Falha ao criar o pedido.', e, s),
47
+ );
48
+ }
49
+
50
+ // Converte a linha do Drift em modelo do domínio; nada de *Data fora daqui.
51
+ Order _toModel(OrderTableData row) =>
52
+ Order(id: row.id, ownerId: row.ownerId, total: row.total);
53
+
54
+ BusinessException _failure(String message, Object error, StackTrace stack) {
55
+ return BusinessException(
56
+ domainName: 'order',
57
+ statusCode: HttpStatus.internalServerError,
58
+ message: message,
59
+ error: error,
60
+ stackTrace: stack,
61
+ );
62
+ }
63
+ }
@@ -0,0 +1,25 @@
1
+ // Caminho: lib/common/infra/database/tables/order_table.dart
2
+ import 'package:drift/drift.dart';
3
+
4
+ /// Pedidos.
5
+ class OrderTable extends Table {
6
+ /// Identificador UUID gerado pelo banco (Postgres).
7
+ /// Em SQLite/sem default no banco, use `clientDefault(() => uuid())`.
8
+ TextColumn get id =>
9
+ text().withDefault(const CustomExpression('gen_random_uuid()'))();
10
+
11
+ /// Dono do pedido.
12
+ TextColumn get ownerId => text()();
13
+
14
+ /// Valor total; a regra de negócio garante `>= 0`, o banco reforça.
15
+ RealColumn get total => real().check(total.isBiggerOrEqualValue(0))();
16
+
17
+ /// Indica exclusão lógica.
18
+ BoolColumn get deleted => boolean().withDefault(const Constant(false))();
19
+
20
+ /// Data de criação (calculada no app, sem exigir migration).
21
+ DateTimeColumn get createdAt => dateTime().clientDefault(DateTime.now)();
22
+
23
+ @override
24
+ Set<Column> get primaryKey => {id};
25
+ }
@@ -0,0 +1,33 @@
1
+ // Caminho: test/orders/adapter/repository/order_repository_test.dart
2
+ import 'package:drift/native.dart';
3
+ import 'package:test/test.dart';
4
+
5
+ void main() {
6
+ late AppDatabase db;
7
+ late OrderRepository repository;
8
+
9
+ setUp(() {
10
+ db = AppDatabase(NativeDatabase.memory());
11
+ repository = OrderRepository(db);
12
+ });
13
+
14
+ tearDown(() => db.close());
15
+
16
+ test('create e findById retornam o pedido', () async {
17
+ final created = await repository.create(ownerId: 'u1', total: 10).run();
18
+ final id = created.getOrElse((_) => throw StateError('falhou')).id;
19
+
20
+ final found = await repository.findById(id).run();
21
+
22
+ expect(found.isRight(), isTrue);
23
+ expect(found.getOrElse((_) => null)?.ownerId, 'u1');
24
+ });
25
+
26
+ test('findById de id inexistente retorna Right(null)', () async {
27
+ final result = await repository.findById('nao-existe').run();
28
+
29
+ expect(result.getOrElse((_) => throw StateError('falhou')), isNull);
30
+ });
31
+ }
32
+ // Obs.: se a tabela usa gen_random_uuid() (Postgres), este teste em SQLite
33
+ // precisa de clientDefault ou de Postgres real (integração).
@@ -0,0 +1,9 @@
1
+ # Postgres — conexão
2
+
3
+ - Pacotes: `drift`, `postgres`, `drift_postgres`; `build.yaml` com `sql.dialects: [postgres]`.
4
+ - Conexão com `PgDatabase(endpoint: ..., settings: ...)`; veja `examples/connection_postgres.dart`.
5
+ - Host, porta, banco, usuário e senha vêm de variáveis de ambiente/segredos; nunca do código.
6
+ - **SSL:** exija TLS em produção (`SslMode.require`/`verifyFull`); desabilite só em desenvolvimento local, decidido por variável de ambiente.
7
+ - Uma instância de banco por processo; reutilize a conexão em vez de abrir uma por requisição. Para alta concorrência considere pool (`PgDatabase.withPool` quando disponível na versão do projeto; confirme na documentação).
8
+ - Falha de conexão no startup deve abortar com mensagem clara (sem imprimir a senha).
9
+ - Projeto existente com singleton global lido do ambiente na importação: mantenha; em código novo, prefira construir na raiz de composição.