@ds9-solutions/mcp 0.3.0 → 0.4.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/README.md +1 -1
- package/content/agents/code-reviewer.agent.md +20 -0
- package/content/agents/db-analyser.agent.md +18 -2
- package/content/agents/security-reviewer.agent.md +21 -1
- package/content/agents/test-writer.agent.md +17 -8
- package/content/skills/dart/SKILL.md +19 -0
- package/content/skills/dart-frog/SKILL.md +22 -1
- package/content/skills/debugger/SKILL.md +144 -0
- package/content/skills/debugger/references/red-flags.md +32 -0
- package/content/skills/debugger/references/report-format.md +65 -0
- package/content/skills/debugger/references/root-cause-tracing.md +42 -0
- package/content/skills/debugger/references/safe-diagnostics.md +47 -0
- package/content/skills/driftorm/SKILL.md +59 -225
- package/content/skills/driftorm/examples/connection_postgres.dart +25 -0
- package/content/skills/driftorm/examples/connection_sqlite.dart +13 -0
- package/content/skills/driftorm/examples/database.dart +25 -0
- package/content/skills/driftorm/examples/migration_manual.dart +24 -0
- package/content/skills/driftorm/examples/migration_step_by_step.dart +24 -0
- package/content/skills/driftorm/examples/pagination.dart +48 -0
- package/content/skills/driftorm/examples/queries.dart +34 -0
- package/content/skills/driftorm/examples/repository_hex.dart +63 -0
- package/content/skills/driftorm/examples/table.dart +25 -0
- package/content/skills/driftorm/examples/test.dart +33 -0
- package/content/skills/driftorm/postgres/connection.md +9 -0
- package/content/skills/driftorm/postgres/migrations.md +11 -0
- package/content/skills/driftorm/postgres/types-and-defaults.md +10 -0
- package/content/skills/driftorm/references/anti-patterns.md +20 -0
- package/content/skills/driftorm/references/checklist.md +17 -0
- package/content/skills/driftorm/references/database-and-connection.md +22 -0
- package/content/skills/driftorm/references/dialects.md +17 -0
- package/content/skills/driftorm/references/drift-files.md +20 -0
- package/content/skills/driftorm/references/migrations.md +26 -0
- package/content/skills/driftorm/references/performance.md +10 -0
- package/content/skills/driftorm/references/queries-and-pagination.md +46 -0
- package/content/skills/driftorm/references/repositories-taskeither.md +29 -0
- package/content/skills/driftorm/references/setup.md +37 -0
- package/content/skills/driftorm/references/tables-and-columns.md +43 -0
- package/content/skills/driftorm/references/testing.md +12 -0
- package/content/skills/driftorm/references/transactions.md +23 -0
- package/content/skills/driftorm/sqlite/connection.md +11 -0
- package/content/skills/driftorm/sqlite/types-and-pragmas.md +9 -0
- package/content/skills/flutter/SKILL.md +19 -0
- package/content/skills/hex-pattern-dart/SKILL.md +21 -0
- package/content/skills/hex-pattern-dart/examples/adapter-in.md +4 -5
- package/content/skills/hex-pattern-dart/references/persistence.md +2 -2
- package/package.json +1 -1
- package/content/skills/docker/SKILL.md +0 -104
- package/content/skills/mysql/SKILL.md +0 -94
- package/content/skills/typescript/SKILL.md +0 -75
|
@@ -1,249 +1,83 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: driftorm
|
|
3
|
-
description: Convenções
|
|
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
|
|
6
|
+
# Drift (persistência) — padrão DS9
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
##
|
|
10
|
+
## Carregamento sob demanda
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
##
|
|
14
|
+
## Estrutura existente vence
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
## Roteamento: pergunta → arquivo
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|