drizzle-multitenant 1.0.6 → 1.0.8
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/package.json +1 -1
- package/.claude/settings.local.json +0 -20
- package/proposals/improvements-from-primesys.md +0 -385
- package/roadmap.md +0 -921
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm install:*)",
|
|
5
|
-
"Bash(npm test)",
|
|
6
|
-
"Bash(npm run build:*)",
|
|
7
|
-
"Bash(npm test:*)",
|
|
8
|
-
"Bash(tree:*)",
|
|
9
|
-
"Bash(find:*)",
|
|
10
|
-
"Bash(mkdir:*)",
|
|
11
|
-
"Bash(node ./bin/drizzle-multitenant.js:*)",
|
|
12
|
-
"Bash(ls:*)",
|
|
13
|
-
"Bash(npm run test:*)",
|
|
14
|
-
"Bash(node ./dist/cli/index.js:*)",
|
|
15
|
-
"Bash(npx tsc:*)",
|
|
16
|
-
"Bash(git filter-branch:*)",
|
|
17
|
-
"Bash(git add:*)"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
# Proposal: Melhorias Identificadas no PrimeSys-v2
|
|
2
|
-
|
|
3
|
-
> **Status**: Proposta
|
|
4
|
-
> **Origem**: Integração com PrimeSys-v2
|
|
5
|
-
> **Data**: 2024-12-23
|
|
6
|
-
|
|
7
|
-
## Contexto
|
|
8
|
-
|
|
9
|
-
Durante a integração do `drizzle-multitenant` no projeto PrimeSys-v2 (multi-tenant SaaS para gestão industrial), foram identificadas melhorias que beneficiariam o pacote e outros usuários.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 1. CLI: Flag `--mark-applied`
|
|
14
|
-
|
|
15
|
-
### Problema
|
|
16
|
-
|
|
17
|
-
Projetos que já têm migrations aplicadas (via scripts legados) precisam sincronizar o tracking sem re-executar as migrations.
|
|
18
|
-
|
|
19
|
-
### Solução
|
|
20
|
-
|
|
21
|
-
Adicionar flag `--mark-applied` ao comando `migrate`:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# Marca migrations como aplicadas sem executar SQL
|
|
25
|
-
npx drizzle-multitenant migrate --all --mark-applied
|
|
26
|
-
|
|
27
|
-
# Para tenant específico
|
|
28
|
-
npx drizzle-multitenant migrate --tenant=abc --mark-applied
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Implementação
|
|
32
|
-
|
|
33
|
-
```typescript
|
|
34
|
-
// src/cli/commands/migrate.ts
|
|
35
|
-
.option('--mark-applied', 'Mark migrations as applied without executing SQL')
|
|
36
|
-
|
|
37
|
-
// No handler
|
|
38
|
-
if (options.markApplied) {
|
|
39
|
-
await migrator.markAllAsApplied(tenantIds);
|
|
40
|
-
} else {
|
|
41
|
-
await migrator.migrateAll({ concurrency, dryRun });
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Benefício
|
|
46
|
-
|
|
47
|
-
- Facilita migração de projetos existentes
|
|
48
|
-
- Útil para sincronizar ambientes de staging/produção
|
|
49
|
-
- Evita necessidade de scripts manuais
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## 2. CLI: Comando `sync`
|
|
54
|
-
|
|
55
|
-
### Problema
|
|
56
|
-
|
|
57
|
-
Detectar e corrigir divergências entre migrations em disco e tracking no banco.
|
|
58
|
-
|
|
59
|
-
### Solução
|
|
60
|
-
|
|
61
|
-
Novo comando `sync` com opções:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
# Mostrar divergências
|
|
65
|
-
npx drizzle-multitenant sync --status
|
|
66
|
-
|
|
67
|
-
# Marcar migrations faltantes como aplicadas
|
|
68
|
-
npx drizzle-multitenant sync --mark-missing
|
|
69
|
-
|
|
70
|
-
# Remover registros órfãos (migrations que não existem mais em disco)
|
|
71
|
-
npx drizzle-multitenant sync --clean-orphans
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Casos de Uso
|
|
75
|
-
|
|
76
|
-
1. **Migration renomeada**: Arquivo renomeado, mas registro antigo no banco
|
|
77
|
-
2. **Migration deletada**: Arquivo removido, registro ainda existe
|
|
78
|
-
3. **Ambiente dessincronizado**: Migrations aplicadas manualmente
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 3. Compatibilidade com Tabelas de Tracking Legadas
|
|
83
|
-
|
|
84
|
-
### Problema
|
|
85
|
-
|
|
86
|
-
Projetos podem usar estruturas de tabela diferentes:
|
|
87
|
-
- Hash-based: `id, hash, created_at`
|
|
88
|
-
- Name-based: `id, name, applied_at` (padrão drizzle-multitenant)
|
|
89
|
-
|
|
90
|
-
### Solução
|
|
91
|
-
|
|
92
|
-
Suportar múltiplos formatos ou migração automática:
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
// tenant.config.ts
|
|
96
|
-
migrations: {
|
|
97
|
-
tenantFolder: "./drizzle/tenant",
|
|
98
|
-
tenantDiscovery: discoverTenants,
|
|
99
|
-
migrationsTable: "__drizzle_migrations",
|
|
100
|
-
|
|
101
|
-
// NOVO: Formato da tabela
|
|
102
|
-
tableFormat: "name", // "name" | "hash" | "auto-detect"
|
|
103
|
-
|
|
104
|
-
// NOVO: Migrar formato automaticamente
|
|
105
|
-
autoMigrateFormat: true,
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Alternativa
|
|
110
|
-
|
|
111
|
-
Script de migração incluído no pacote:
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
npx drizzle-multitenant migrate-tracking-format --from=hash --to=name
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## 4. Health Check API
|
|
120
|
-
|
|
121
|
-
### Problema
|
|
122
|
-
|
|
123
|
-
Aplicações precisam verificar saúde dos pools para load balancers e monitoring.
|
|
124
|
-
|
|
125
|
-
### Solução
|
|
126
|
-
|
|
127
|
-
Método `healthCheck()` no TenantManager:
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
const manager = createTenantManager(config);
|
|
131
|
-
|
|
132
|
-
const health = await manager.healthCheck();
|
|
133
|
-
// {
|
|
134
|
-
// healthy: true,
|
|
135
|
-
// pools: [
|
|
136
|
-
// { tenantId: 'abc', status: 'ok', connections: 5, idle: 3 },
|
|
137
|
-
// { tenantId: 'def', status: 'degraded', connections: 1, idle: 0 },
|
|
138
|
-
// ],
|
|
139
|
-
// sharedDb: { status: 'ok', connections: 10 },
|
|
140
|
-
// timestamp: '2024-12-23T10:30:00Z'
|
|
141
|
-
// }
|
|
142
|
-
|
|
143
|
-
// Endpoint para load balancers
|
|
144
|
-
app.get('/health/db', async (req, res) => {
|
|
145
|
-
const health = await manager.healthCheck();
|
|
146
|
-
res.status(health.healthy ? 200 : 503).json(health);
|
|
147
|
-
});
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Já no Roadmap
|
|
151
|
-
|
|
152
|
-
Previsto para v1.1.0 (Resiliência e Observabilidade).
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## 5. Métricas Prometheus
|
|
157
|
-
|
|
158
|
-
### Problema
|
|
159
|
-
|
|
160
|
-
Monitoramento de pools e queries é essencial para produção.
|
|
161
|
-
|
|
162
|
-
### Solução
|
|
163
|
-
|
|
164
|
-
Exportar métricas no formato Prometheus:
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
// Configuração
|
|
168
|
-
const config = defineConfig({
|
|
169
|
-
metrics: {
|
|
170
|
-
enabled: true,
|
|
171
|
-
prefix: 'drizzle_multitenant',
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Endpoint
|
|
176
|
-
app.get('/metrics', (req, res) => {
|
|
177
|
-
res.set('Content-Type', 'text/plain');
|
|
178
|
-
res.send(manager.getPrometheusMetrics());
|
|
179
|
-
});
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
**Métricas sugeridas:**
|
|
183
|
-
```
|
|
184
|
-
drizzle_multitenant_pool_count 15
|
|
185
|
-
drizzle_multitenant_pool_connections_active{tenant="abc"} 3
|
|
186
|
-
drizzle_multitenant_pool_connections_idle{tenant="abc"} 7
|
|
187
|
-
drizzle_multitenant_pool_evictions_total 42
|
|
188
|
-
drizzle_multitenant_query_duration_seconds_bucket{le="0.1"} 1024
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Já no Roadmap
|
|
192
|
-
|
|
193
|
-
Previsto para v1.1.0 (Resiliência e Observabilidade).
|
|
194
|
-
|
|
195
|
-
---
|
|
196
|
-
|
|
197
|
-
## 6. NestJS: `@TenantId()` Parameter Decorator
|
|
198
|
-
|
|
199
|
-
### Problema
|
|
200
|
-
|
|
201
|
-
Extrair `tenantId` do request requer código boilerplate:
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
@Get()
|
|
205
|
-
async getData(@Param('empresaId') empresaId: string) {
|
|
206
|
-
return this.service.getData(empresaId);
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Solução
|
|
211
|
-
|
|
212
|
-
Decorator que extrai automaticamente baseado na config:
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
import { TenantId } from 'drizzle-multitenant/nestjs';
|
|
216
|
-
|
|
217
|
-
@Get()
|
|
218
|
-
async getData(@TenantId() tenantId: string) {
|
|
219
|
-
return this.service.getData(tenantId);
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Implementação
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
// src/integrations/nestjs/decorators/tenant-id.decorator.ts
|
|
227
|
-
export const TenantId = createParamDecorator(
|
|
228
|
-
(data: unknown, ctx: ExecutionContext): string => {
|
|
229
|
-
const request = ctx.switchToHttp().getRequest();
|
|
230
|
-
// Usa extractTenantId configurado no TenantModule
|
|
231
|
-
return request.tenantId; // Setado pelo middleware
|
|
232
|
-
},
|
|
233
|
-
);
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## 7. Pool Warmup
|
|
239
|
-
|
|
240
|
-
### Problema
|
|
241
|
-
|
|
242
|
-
Primeiro request para um tenant tem latência maior (cold start do pool).
|
|
243
|
-
|
|
244
|
-
### Solução
|
|
245
|
-
|
|
246
|
-
API para pré-aquecer pools:
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
// Warmup de tenants específicos
|
|
250
|
-
await manager.warmup(['tenant-1', 'tenant-2', 'tenant-3']);
|
|
251
|
-
|
|
252
|
-
// Warmup de todos (usar com cuidado)
|
|
253
|
-
const tenants = await discoverTenants();
|
|
254
|
-
await manager.warmup(tenants.slice(0, 20)); // Top 20 mais ativos
|
|
255
|
-
|
|
256
|
-
// No bootstrap da aplicação NestJS
|
|
257
|
-
@Injectable()
|
|
258
|
-
export class WarmupService implements OnApplicationBootstrap {
|
|
259
|
-
constructor(@InjectTenantManager() private manager: TenantManager) {}
|
|
260
|
-
|
|
261
|
-
async onApplicationBootstrap() {
|
|
262
|
-
const topTenants = await this.getTopTenants();
|
|
263
|
-
await this.manager.warmup(topTenants);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
## 8. Debug Mode Aprimorado
|
|
271
|
-
|
|
272
|
-
### Problema
|
|
273
|
-
|
|
274
|
-
Debugar queries multi-tenant é difícil sem contexto.
|
|
275
|
-
|
|
276
|
-
### Solução
|
|
277
|
-
|
|
278
|
-
Modo debug que loga queries com tenant context:
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
const config = defineConfig({
|
|
282
|
-
debug: {
|
|
283
|
-
enabled: process.env.NODE_ENV === 'development',
|
|
284
|
-
logQueries: true,
|
|
285
|
-
logPoolEvents: true,
|
|
286
|
-
slowQueryThreshold: 1000, // ms
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// Output
|
|
291
|
-
// [drizzle-multitenant] tenant=abc query="SELECT * FROM produtos" duration=45ms
|
|
292
|
-
// [drizzle-multitenant] tenant=abc SLOW_QUERY query="SELECT..." duration=1523ms
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## 9. Retry Logic para Conexões
|
|
298
|
-
|
|
299
|
-
### Problema
|
|
300
|
-
|
|
301
|
-
Conexões podem falhar temporariamente (network issues, DB restart).
|
|
302
|
-
|
|
303
|
-
### Solução
|
|
304
|
-
|
|
305
|
-
Retry automático com backoff exponencial:
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
const config = defineConfig({
|
|
309
|
-
connection: {
|
|
310
|
-
url: process.env.DATABASE_URL!,
|
|
311
|
-
retry: {
|
|
312
|
-
maxAttempts: 3,
|
|
313
|
-
initialDelayMs: 100,
|
|
314
|
-
maxDelayMs: 5000,
|
|
315
|
-
backoffMultiplier: 2,
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Já no Roadmap
|
|
322
|
-
|
|
323
|
-
Previsto para v1.1.0 (Resiliência e Observabilidade).
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## 10. Cross-Schema Query Improvements
|
|
328
|
-
|
|
329
|
-
### Problema
|
|
330
|
-
|
|
331
|
-
Queries que juntam tenant + shared tables são verbosas.
|
|
332
|
-
|
|
333
|
-
### Solução Atual (v1.0)
|
|
334
|
-
|
|
335
|
-
```typescript
|
|
336
|
-
const query = createCrossSchemaQuery({
|
|
337
|
-
tenantDb: tenants.getDb('tenant-123'),
|
|
338
|
-
sharedDb: tenants.getSharedDb(),
|
|
339
|
-
tenantSchema: 'tenant_123',
|
|
340
|
-
sharedSchema: 'public',
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### Solução Proposta
|
|
345
|
-
|
|
346
|
-
Helper mais simples:
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
// Usando TenantDbFactory
|
|
350
|
-
const db = this.dbFactory.getDb(tenantId);
|
|
351
|
-
|
|
352
|
-
// Novo: withShared() helper
|
|
353
|
-
const result = await db
|
|
354
|
-
.withShared(this.sharedDb)
|
|
355
|
-
.select({
|
|
356
|
-
pedidoId: pedido.id,
|
|
357
|
-
workflowNome: workflowStep.nome, // da tabela public
|
|
358
|
-
})
|
|
359
|
-
.from(pedido)
|
|
360
|
-
.leftJoin(workflowStep, eq(pedido.workflowStepId, workflowStep.id));
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
## Priorização Sugerida
|
|
366
|
-
|
|
367
|
-
| Melhoria | Esforço | Impacto | Prioridade |
|
|
368
|
-
|----------|---------|---------|------------|
|
|
369
|
-
| `--mark-applied` flag | 2h | Alto | P0 |
|
|
370
|
-
| `@TenantId()` decorator | 1h | Médio | P1 |
|
|
371
|
-
| Pool warmup | 2h | Médio | P1 |
|
|
372
|
-
| Health check API | 4h | Alto | P1 |
|
|
373
|
-
| Debug mode | 3h | Médio | P2 |
|
|
374
|
-
| Retry logic | 4h | Alto | P2 |
|
|
375
|
-
| Métricas Prometheus | 6h | Alto | P2 |
|
|
376
|
-
| Sync command | 4h | Médio | P2 |
|
|
377
|
-
| Tabela legada compat | 4h | Baixo | P3 |
|
|
378
|
-
| Cross-schema helper | 6h | Médio | P3 |
|
|
379
|
-
|
|
380
|
-
---
|
|
381
|
-
|
|
382
|
-
## Referências
|
|
383
|
-
|
|
384
|
-
- [PrimeSys-v2 Migration Proposal](../../PrimeSys-v2/proposals/backlog/drizzle-multitenant-migration.md)
|
|
385
|
-
- [drizzle-multitenant Roadmap](../roadmap.md)
|
package/roadmap.md
DELETED
|
@@ -1,921 +0,0 @@
|
|
|
1
|
-
# drizzle-multitenant - Roadmap
|
|
2
|
-
|
|
3
|
-
> Multi-tenancy toolkit for Drizzle ORM with schema isolation, tenant context, and parallel migrations.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Versões Completas
|
|
8
|
-
|
|
9
|
-
### v0.1.0 - Core
|
|
10
|
-
- [x] `defineConfig` e tipos
|
|
11
|
-
- [x] `createTenantManager` com pool management
|
|
12
|
-
- [x] `getDb()` e `getSharedDb()`
|
|
13
|
-
- [x] Cleanup automático de pools (LRU)
|
|
14
|
-
- [x] Testes unitários
|
|
15
|
-
|
|
16
|
-
### v0.2.0 - Context
|
|
17
|
-
- [x] `createTenantContext` com AsyncLocalStorage
|
|
18
|
-
- [x] Express middleware
|
|
19
|
-
- [x] Fastify plugin
|
|
20
|
-
- [x] Testes de integração
|
|
21
|
-
|
|
22
|
-
### v0.3.0 - Migrations
|
|
23
|
-
- [x] CLI base (generate, migrate, status)
|
|
24
|
-
- [x] Parallel migration engine
|
|
25
|
-
- [x] tenant:create e tenant:drop
|
|
26
|
-
- [x] Hooks de migration
|
|
27
|
-
|
|
28
|
-
### v0.4.0 - Cross-Schema
|
|
29
|
-
- [x] `createCrossSchemaQuery`
|
|
30
|
-
- [x] `withSharedLookup` helper
|
|
31
|
-
- [x] Type inference completo
|
|
32
|
-
|
|
33
|
-
### v0.5.0 - NestJS
|
|
34
|
-
- [x] `TenantModule.forRoot()`
|
|
35
|
-
- [x] `@InjectTenantDb()` decorator
|
|
36
|
-
- [x] `@InjectTenantContext()` decorator
|
|
37
|
-
- [x] Guards e interceptors
|
|
38
|
-
|
|
39
|
-
### v1.0.0 - Production Ready
|
|
40
|
-
- [x] Documentação completa (README.md)
|
|
41
|
-
- [x] Package publicado no npm
|
|
42
|
-
- [x] 148 testes passando
|
|
43
|
-
- [x] Licença MIT
|
|
44
|
-
|
|
45
|
-
### v1.0.3 - NestJS DX Improvements
|
|
46
|
-
- [x] `TenantDbFactory` para singleton services (cron jobs, event handlers)
|
|
47
|
-
- [x] `@InjectTenantDbFactory()` decorator
|
|
48
|
-
- [x] Debug utilities para proxies (`__debug`, `__tenantId`, `__isProxy`)
|
|
49
|
-
- [x] `console.log(tenantDb)` mostra informações úteis
|
|
50
|
-
- [x] CLI `migrationsTable` config support
|
|
51
|
-
- [x] 154 testes passando
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## Próximas Versões
|
|
56
|
-
|
|
57
|
-
### v1.1.0 - Resiliência e Observabilidade
|
|
58
|
-
|
|
59
|
-
#### Retry Logic para Conexões
|
|
60
|
-
Conexões podem falhar temporariamente. Adicionar retry automático com backoff exponencial.
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import { defineConfig } from 'drizzle-multitenant';
|
|
64
|
-
|
|
65
|
-
export default defineConfig({
|
|
66
|
-
connection: {
|
|
67
|
-
url: process.env.DATABASE_URL!,
|
|
68
|
-
retry: {
|
|
69
|
-
maxAttempts: 3,
|
|
70
|
-
initialDelayMs: 100,
|
|
71
|
-
maxDelayMs: 5000,
|
|
72
|
-
backoffMultiplier: 2,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
// ...
|
|
76
|
-
});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
#### Health Checks
|
|
80
|
-
Verificar saúde dos pools e conexões.
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
const manager = createTenantManager(config);
|
|
84
|
-
|
|
85
|
-
// Verificar saúde de todos os pools
|
|
86
|
-
const health = await manager.healthCheck();
|
|
87
|
-
// {
|
|
88
|
-
// healthy: true,
|
|
89
|
-
// pools: [
|
|
90
|
-
// { tenantId: 'abc', status: 'ok', connections: 5 },
|
|
91
|
-
// { tenantId: 'def', status: 'degraded', connections: 1 },
|
|
92
|
-
// ],
|
|
93
|
-
// sharedDb: 'ok',
|
|
94
|
-
// timestamp: '2024-01-15T10:30:00Z'
|
|
95
|
-
// }
|
|
96
|
-
|
|
97
|
-
// Endpoint para load balancers
|
|
98
|
-
app.get('/health', async (req, res) => {
|
|
99
|
-
const health = await manager.healthCheck();
|
|
100
|
-
res.status(health.healthy ? 200 : 503).json(health);
|
|
101
|
-
});
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
#### Métricas Prometheus
|
|
105
|
-
Expor métricas no formato Prometheus para monitoramento.
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
import { defineConfig } from 'drizzle-multitenant';
|
|
109
|
-
|
|
110
|
-
export default defineConfig({
|
|
111
|
-
// ...
|
|
112
|
-
metrics: {
|
|
113
|
-
enabled: true,
|
|
114
|
-
prefix: 'drizzle_multitenant',
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Endpoint para Prometheus
|
|
119
|
-
app.get('/metrics', async (req, res) => {
|
|
120
|
-
const metrics = manager.getMetrics();
|
|
121
|
-
res.set('Content-Type', 'text/plain');
|
|
122
|
-
res.send(metrics);
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Métricas expostas:
|
|
127
|
-
```
|
|
128
|
-
drizzle_multitenant_pool_count 15
|
|
129
|
-
drizzle_multitenant_pool_connections_active{tenant="abc"} 3
|
|
130
|
-
drizzle_multitenant_pool_connections_idle{tenant="abc"} 7
|
|
131
|
-
drizzle_multitenant_query_duration_seconds_bucket{le="0.1"} 1024
|
|
132
|
-
drizzle_multitenant_pool_evictions_total 42
|
|
133
|
-
drizzle_multitenant_errors_total{type="connection"} 3
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
#### Structured Logging
|
|
137
|
-
Integração com loggers populares (pino, winston).
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
import pino from 'pino';
|
|
141
|
-
|
|
142
|
-
const logger = pino({ level: 'info' });
|
|
143
|
-
|
|
144
|
-
export default defineConfig({
|
|
145
|
-
// ...
|
|
146
|
-
hooks: {
|
|
147
|
-
logger: {
|
|
148
|
-
provider: logger,
|
|
149
|
-
level: 'info',
|
|
150
|
-
// Logs estruturados automaticamente
|
|
151
|
-
// { tenant: 'abc', event: 'pool_created', duration: 45 }
|
|
152
|
-
},
|
|
153
|
-
onPoolCreated: (tenantId) => {
|
|
154
|
-
logger.info({ tenant: tenantId }, 'Pool created');
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Checklist v1.1.0:**
|
|
161
|
-
- [ ] Retry logic com backoff exponencial
|
|
162
|
-
- [ ] `manager.healthCheck()` API
|
|
163
|
-
- [ ] Métricas Prometheus
|
|
164
|
-
- [ ] Integração com pino/winston
|
|
165
|
-
- [ ] Testes unitários e integração
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
### v1.2.0 - Segurança
|
|
170
|
-
|
|
171
|
-
#### Schema Name Sanitization
|
|
172
|
-
Validar e sanitizar nomes de schema para prevenir SQL injection.
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
import { defineConfig } from 'drizzle-multitenant';
|
|
176
|
-
|
|
177
|
-
export default defineConfig({
|
|
178
|
-
isolation: {
|
|
179
|
-
strategy: 'schema',
|
|
180
|
-
schemaNameTemplate: (tenantId) => `tenant_${tenantId}`,
|
|
181
|
-
// Sanitização automática habilitada por padrão
|
|
182
|
-
sanitize: {
|
|
183
|
-
enabled: true,
|
|
184
|
-
maxLength: 63, // Limite PostgreSQL
|
|
185
|
-
allowedChars: /^[a-z0-9_]+$/,
|
|
186
|
-
reservedNames: ['public', 'pg_catalog', 'information_schema'],
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Throws error se nome inválido
|
|
192
|
-
manager.getDb('tenant; DROP TABLE users;--'); // Error: Invalid tenant ID
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
#### Rate Limiting por Tenant
|
|
196
|
-
Limitar queries por tenant para prevenir abuso.
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
export default defineConfig({
|
|
200
|
-
// ...
|
|
201
|
-
rateLimit: {
|
|
202
|
-
enabled: true,
|
|
203
|
-
maxQueriesPerSecond: 100,
|
|
204
|
-
maxConnectionsPerTenant: 5,
|
|
205
|
-
onLimitExceeded: (tenantId, limit) => {
|
|
206
|
-
logger.warn({ tenant: tenantId, limit }, 'Rate limit exceeded');
|
|
207
|
-
// 'throttle' | 'reject' | 'queue'
|
|
208
|
-
return 'throttle';
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
#### Tenant Isolation Audit
|
|
215
|
-
Auditoria para garantir isolamento entre tenants.
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
export default defineConfig({
|
|
219
|
-
// ...
|
|
220
|
-
audit: {
|
|
221
|
-
enabled: true,
|
|
222
|
-
logQueries: true,
|
|
223
|
-
detectCrossSchemaAccess: true,
|
|
224
|
-
onViolation: async (event) => {
|
|
225
|
-
// {
|
|
226
|
-
// type: 'cross_schema_access',
|
|
227
|
-
// tenant: 'abc',
|
|
228
|
-
// query: 'SELECT * FROM tenant_def.users',
|
|
229
|
-
// timestamp: Date
|
|
230
|
-
// }
|
|
231
|
-
await sendAlert(event);
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Checklist v1.2.0:**
|
|
238
|
-
- [ ] Schema name sanitization
|
|
239
|
-
- [ ] Rate limiting por tenant
|
|
240
|
-
- [ ] Audit logging
|
|
241
|
-
- [ ] Detecção de cross-schema access
|
|
242
|
-
- [ ] Testes de segurança
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
### v1.3.0 - Performance
|
|
247
|
-
|
|
248
|
-
#### Connection Queue
|
|
249
|
-
Gerenciar overflow de pools com queue de espera.
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
export default defineConfig({
|
|
253
|
-
isolation: {
|
|
254
|
-
maxPools: 50,
|
|
255
|
-
pooling: {
|
|
256
|
-
strategy: 'queue', // 'queue' | 'reject' | 'evict-lru'
|
|
257
|
-
queueTimeout: 5000,
|
|
258
|
-
maxWaitingRequests: 100,
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Eventos de queue
|
|
264
|
-
hooks: {
|
|
265
|
-
onQueueFull: (tenantId) => {
|
|
266
|
-
logger.warn({ tenant: tenantId }, 'Connection queue full');
|
|
267
|
-
},
|
|
268
|
-
onQueueTimeout: (tenantId, waitTime) => {
|
|
269
|
-
logger.error({ tenant: tenantId, waitTime }, 'Queue timeout');
|
|
270
|
-
},
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
#### Query Caching
|
|
275
|
-
Cache opcional para queries repetidas.
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
import { createTenantManager } from 'drizzle-multitenant';
|
|
279
|
-
import Redis from 'ioredis';
|
|
280
|
-
|
|
281
|
-
const redis = new Redis();
|
|
282
|
-
|
|
283
|
-
const manager = createTenantManager({
|
|
284
|
-
...config,
|
|
285
|
-
cache: {
|
|
286
|
-
enabled: true,
|
|
287
|
-
provider: redis, // ou 'memory'
|
|
288
|
-
defaultTtlMs: 60000,
|
|
289
|
-
maxSize: 10000, // para memory provider
|
|
290
|
-
keyPrefix: 'dmt:',
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// Uso no código
|
|
295
|
-
const db = manager.getDb('tenant-123');
|
|
296
|
-
|
|
297
|
-
// Query com cache
|
|
298
|
-
const users = await db
|
|
299
|
-
.select()
|
|
300
|
-
.from(schema.users)
|
|
301
|
-
.where(eq(schema.users.active, true))
|
|
302
|
-
.$cache({ ttl: 5000, key: 'active-users' });
|
|
303
|
-
|
|
304
|
-
// Invalidar cache
|
|
305
|
-
await manager.invalidateCache('tenant-123', 'active-users');
|
|
306
|
-
await manager.invalidateTenantCache('tenant-123'); // todo cache do tenant
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
#### Prepared Statements Pool
|
|
310
|
-
Reutilizar prepared statements entre requests.
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
export default defineConfig({
|
|
314
|
-
connection: {
|
|
315
|
-
url: process.env.DATABASE_URL!,
|
|
316
|
-
preparedStatements: {
|
|
317
|
-
enabled: true,
|
|
318
|
-
maxPerTenant: 100,
|
|
319
|
-
ttlMs: 3600000, // 1 hora
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**Checklist v1.3.0:**
|
|
326
|
-
- [ ] Connection queue com timeout
|
|
327
|
-
- [ ] Query caching (memory + Redis)
|
|
328
|
-
- [ ] Cache invalidation API
|
|
329
|
-
- [ ] Prepared statements pool
|
|
330
|
-
- [ ] Benchmarks de performance
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
### v1.4.0 - Novas Estratégias de Isolamento
|
|
335
|
-
|
|
336
|
-
#### Row-Level Security (RLS)
|
|
337
|
-
Isolamento por linha usando RLS do PostgreSQL.
|
|
338
|
-
|
|
339
|
-
```typescript
|
|
340
|
-
export default defineConfig({
|
|
341
|
-
isolation: {
|
|
342
|
-
strategy: 'row',
|
|
343
|
-
tenantColumn: 'tenant_id',
|
|
344
|
-
enableRLS: true,
|
|
345
|
-
},
|
|
346
|
-
schemas: {
|
|
347
|
-
tenant: tenantSchema,
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// Gera automaticamente:
|
|
352
|
-
// CREATE POLICY tenant_isolation ON users
|
|
353
|
-
// USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
|
354
|
-
|
|
355
|
-
// Uso transparente
|
|
356
|
-
const db = manager.getDb('tenant-123');
|
|
357
|
-
const users = await db.select().from(schema.users);
|
|
358
|
-
// WHERE tenant_id = 'tenant-123' aplicado automaticamente
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
#### Database-per-Tenant
|
|
362
|
-
Isolamento completo por database.
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
export default defineConfig({
|
|
366
|
-
isolation: {
|
|
367
|
-
strategy: 'database',
|
|
368
|
-
databaseNameTemplate: (tenantId) => `db_${tenantId}`,
|
|
369
|
-
createDatabase: true, // criar automaticamente
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// Cada tenant tem seu próprio database
|
|
374
|
-
// db_tenant_abc, db_tenant_def, etc.
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
#### Hybrid Strategy
|
|
378
|
-
Combinar estratégias para diferentes tiers de tenants.
|
|
379
|
-
|
|
380
|
-
```typescript
|
|
381
|
-
export default defineConfig({
|
|
382
|
-
isolation: {
|
|
383
|
-
strategy: 'hybrid',
|
|
384
|
-
default: 'row', // tenants pequenos
|
|
385
|
-
rules: [
|
|
386
|
-
{
|
|
387
|
-
condition: async (tenantId) => {
|
|
388
|
-
const plan = await getTenantPlan(tenantId);
|
|
389
|
-
return plan === 'enterprise';
|
|
390
|
-
},
|
|
391
|
-
strategy: 'schema', // tenants enterprise
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
condition: async (tenantId) => {
|
|
395
|
-
const rows = await getTenantRowCount(tenantId);
|
|
396
|
-
return rows > 100000;
|
|
397
|
-
},
|
|
398
|
-
strategy: 'schema', // tenants grandes
|
|
399
|
-
},
|
|
400
|
-
],
|
|
401
|
-
onPromotion: async (tenantId, from, to) => {
|
|
402
|
-
// Migrar dados de RLS para schema dedicado
|
|
403
|
-
await migrateDataToSchema(tenantId);
|
|
404
|
-
logger.info({ tenant: tenantId, from, to }, 'Tenant promoted');
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
**Checklist v1.4.0:**
|
|
411
|
-
- [ ] Row-Level Security (RLS)
|
|
412
|
-
- [ ] Geração automática de policies
|
|
413
|
-
- [ ] Database-per-tenant strategy
|
|
414
|
-
- [ ] Hybrid strategy com regras
|
|
415
|
-
- [ ] Migração entre estratégias
|
|
416
|
-
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
### v1.5.0 - Developer Experience
|
|
420
|
-
|
|
421
|
-
#### CLI Interativo
|
|
422
|
-
Modo interativo para operações comuns.
|
|
423
|
-
|
|
424
|
-
```bash
|
|
425
|
-
$ npx drizzle-multitenant
|
|
426
|
-
|
|
427
|
-
? What do you want to do? (Use arrow keys)
|
|
428
|
-
❯ Migrate all tenants
|
|
429
|
-
Check migration status
|
|
430
|
-
Create new tenant
|
|
431
|
-
Drop tenant
|
|
432
|
-
View pool statistics
|
|
433
|
-
Generate migration
|
|
434
|
-
Exit
|
|
435
|
-
|
|
436
|
-
? Select tenants to migrate:
|
|
437
|
-
[x] tenant_abc (2 pending)
|
|
438
|
-
[x] tenant_def (2 pending)
|
|
439
|
-
[ ] tenant_ghi (up to date)
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
#### Tenant Seeding
|
|
443
|
-
Popular dados iniciais em tenants.
|
|
444
|
-
|
|
445
|
-
```typescript
|
|
446
|
-
// seeds/initial.ts
|
|
447
|
-
import { SeedFunction } from 'drizzle-multitenant';
|
|
448
|
-
|
|
449
|
-
export const seed: SeedFunction = async (db, tenantId) => {
|
|
450
|
-
await db.insert(roles).values([
|
|
451
|
-
{ name: 'admin', permissions: ['*'] },
|
|
452
|
-
{ name: 'user', permissions: ['read'] },
|
|
453
|
-
]);
|
|
454
|
-
|
|
455
|
-
await db.insert(settings).values({
|
|
456
|
-
tenantId,
|
|
457
|
-
theme: 'light',
|
|
458
|
-
language: 'pt-BR',
|
|
459
|
-
});
|
|
460
|
-
};
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
```bash
|
|
464
|
-
# CLI
|
|
465
|
-
npx drizzle-multitenant seed --tenant=abc --file=./seeds/initial.ts
|
|
466
|
-
npx drizzle-multitenant seed --all --file=./seeds/initial.ts
|
|
467
|
-
|
|
468
|
-
# Programático
|
|
469
|
-
await migrator.seedTenant('abc', seed);
|
|
470
|
-
await migrator.seedAll(seed, { concurrency: 10 });
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
#### Schema Drift Detection
|
|
474
|
-
Detectar divergências entre schema esperado e atual.
|
|
475
|
-
|
|
476
|
-
```bash
|
|
477
|
-
$ npx drizzle-multitenant diff --tenant=abc
|
|
478
|
-
|
|
479
|
-
Schema drift detected in tenant_abc:
|
|
480
|
-
|
|
481
|
-
Missing columns:
|
|
482
|
-
- users.avatar_url (varchar)
|
|
483
|
-
- users.last_login (timestamp)
|
|
484
|
-
|
|
485
|
-
Extra columns:
|
|
486
|
-
- users.legacy_field (will be removed)
|
|
487
|
-
|
|
488
|
-
Index differences:
|
|
489
|
-
- Missing: idx_users_email
|
|
490
|
-
- Extra: idx_legacy_lookup
|
|
491
|
-
|
|
492
|
-
Run 'drizzle-multitenant migrate --tenant=abc' to fix.
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
#### Tenant Cloning
|
|
496
|
-
Clonar tenant para desenvolvimento/teste.
|
|
497
|
-
|
|
498
|
-
```bash
|
|
499
|
-
# CLI
|
|
500
|
-
npx drizzle-multitenant tenant:clone \
|
|
501
|
-
--from=production-tenant \
|
|
502
|
-
--to=dev-tenant \
|
|
503
|
-
--include-data \
|
|
504
|
-
--anonymize # GDPR compliance
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
// Programático
|
|
509
|
-
await migrator.cloneTenant('source', 'target', {
|
|
510
|
-
includeData: true,
|
|
511
|
-
anonymize: {
|
|
512
|
-
enabled: true,
|
|
513
|
-
rules: {
|
|
514
|
-
users: {
|
|
515
|
-
email: (val) => `user-${hash(val)}@example.com`,
|
|
516
|
-
name: () => faker.person.fullName(),
|
|
517
|
-
phone: () => null,
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
**Checklist v1.5.0:**
|
|
525
|
-
- [ ] CLI interativo com inquirer
|
|
526
|
-
- [ ] Tenant seeding API
|
|
527
|
-
- [ ] Schema drift detection
|
|
528
|
-
- [ ] Tenant cloning com anonymization
|
|
529
|
-
- [ ] Documentação interativa
|
|
530
|
-
|
|
531
|
-
---
|
|
532
|
-
|
|
533
|
-
### v1.6.0 - Integrações Avançadas
|
|
534
|
-
|
|
535
|
-
#### tRPC Integration
|
|
536
|
-
Middleware para tRPC.
|
|
537
|
-
|
|
538
|
-
```typescript
|
|
539
|
-
import { initTRPC } from '@trpc/server';
|
|
540
|
-
import { createTRPCMiddleware } from 'drizzle-multitenant/trpc';
|
|
541
|
-
|
|
542
|
-
const t = initTRPC.context<Context>().create();
|
|
543
|
-
|
|
544
|
-
const tenantMiddleware = createTRPCMiddleware({
|
|
545
|
-
manager,
|
|
546
|
-
extractTenantId: (ctx) => ctx.req.headers['x-tenant-id'],
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
export const protectedProcedure = t.procedure.use(tenantMiddleware);
|
|
550
|
-
|
|
551
|
-
// Uso
|
|
552
|
-
export const userRouter = router({
|
|
553
|
-
list: protectedProcedure.query(async ({ ctx }) => {
|
|
554
|
-
// ctx.tenantDb já configurado
|
|
555
|
-
return ctx.tenantDb.select().from(users);
|
|
556
|
-
}),
|
|
557
|
-
});
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
#### GraphQL Context
|
|
561
|
-
Integração com Apollo Server e GraphQL Yoga.
|
|
562
|
-
|
|
563
|
-
```typescript
|
|
564
|
-
import { ApolloServer } from '@apollo/server';
|
|
565
|
-
import { createGraphQLContext } from 'drizzle-multitenant/graphql';
|
|
566
|
-
|
|
567
|
-
const server = new ApolloServer({
|
|
568
|
-
typeDefs,
|
|
569
|
-
resolvers,
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
const { url } = await startStandaloneServer(server, {
|
|
573
|
-
context: createGraphQLContext({
|
|
574
|
-
manager,
|
|
575
|
-
extractTenantId: (req) => req.headers['x-tenant-id'],
|
|
576
|
-
}),
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// Nos resolvers
|
|
580
|
-
const resolvers = {
|
|
581
|
-
Query: {
|
|
582
|
-
users: async (_, __, { tenantDb }) => {
|
|
583
|
-
return tenantDb.select().from(users);
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
#### BullMQ Integration
|
|
590
|
-
Contexto de tenant em jobs de background.
|
|
591
|
-
|
|
592
|
-
```typescript
|
|
593
|
-
import { Queue, Worker } from 'bullmq';
|
|
594
|
-
import { createBullMQPlugin } from 'drizzle-multitenant/bullmq';
|
|
595
|
-
|
|
596
|
-
const queue = new Queue('emails');
|
|
597
|
-
|
|
598
|
-
// Job sempre inclui tenantId
|
|
599
|
-
await queue.add('send-welcome', {
|
|
600
|
-
tenantId: 'abc', // obrigatório
|
|
601
|
-
userId: '123',
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
// Worker com contexto automático
|
|
605
|
-
const worker = new Worker(
|
|
606
|
-
'emails',
|
|
607
|
-
async (job) => {
|
|
608
|
-
// tenantDb configurado automaticamente via job.data.tenantId
|
|
609
|
-
const { tenantDb } = job;
|
|
610
|
-
const user = await tenantDb
|
|
611
|
-
.select()
|
|
612
|
-
.from(users)
|
|
613
|
-
.where(eq(users.id, job.data.userId));
|
|
614
|
-
|
|
615
|
-
await sendEmail(user.email);
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
plugins: [createBullMQPlugin({ manager })],
|
|
619
|
-
}
|
|
620
|
-
);
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
**Checklist v1.6.0:**
|
|
624
|
-
- [ ] tRPC middleware
|
|
625
|
-
- [ ] Apollo Server context
|
|
626
|
-
- [ ] GraphQL Yoga context
|
|
627
|
-
- [ ] BullMQ plugin
|
|
628
|
-
- [ ] Exemplos e documentação
|
|
629
|
-
|
|
630
|
-
---
|
|
631
|
-
|
|
632
|
-
### v2.0.0 - Enterprise Features
|
|
633
|
-
|
|
634
|
-
#### Multi-Region Support
|
|
635
|
-
Suporte para tenants em diferentes regiões.
|
|
636
|
-
|
|
637
|
-
```typescript
|
|
638
|
-
export default defineConfig({
|
|
639
|
-
regions: {
|
|
640
|
-
'us-east': {
|
|
641
|
-
url: process.env.US_EAST_DB_URL!,
|
|
642
|
-
default: true,
|
|
643
|
-
},
|
|
644
|
-
'eu-west': {
|
|
645
|
-
url: process.env.EU_WEST_DB_URL!,
|
|
646
|
-
},
|
|
647
|
-
'ap-south': {
|
|
648
|
-
url: process.env.AP_SOUTH_DB_URL!,
|
|
649
|
-
},
|
|
650
|
-
},
|
|
651
|
-
tenantRegion: async (tenantId) => {
|
|
652
|
-
const tenant = await getTenant(tenantId);
|
|
653
|
-
return tenant.region; // 'us-east' | 'eu-west' | 'ap-south'
|
|
654
|
-
},
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// Transparente para o código
|
|
658
|
-
const db = manager.getDb('tenant-123'); // conecta na região correta
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
#### Backup & Restore
|
|
662
|
-
Backup e restore por tenant.
|
|
663
|
-
|
|
664
|
-
```typescript
|
|
665
|
-
// Backup
|
|
666
|
-
await migrator.backupTenant('abc', {
|
|
667
|
-
output: './backups/abc-2024-01-15.sql',
|
|
668
|
-
format: 'sql', // 'sql' | 'custom' | 'directory'
|
|
669
|
-
compress: true,
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
// Backup para S3
|
|
673
|
-
await migrator.backupTenant('abc', {
|
|
674
|
-
output: 's3://my-bucket/backups/abc.sql.gz',
|
|
675
|
-
s3: { region: 'us-east-1' },
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// Restore
|
|
679
|
-
await migrator.restoreTenant('abc', {
|
|
680
|
-
input: './backups/abc-2024-01-15.sql',
|
|
681
|
-
dropExisting: true,
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
// Restore para novo tenant
|
|
685
|
-
await migrator.restoreTenant('abc-copy', {
|
|
686
|
-
input: './backups/abc-2024-01-15.sql',
|
|
687
|
-
createSchema: true,
|
|
688
|
-
});
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
#### Tenant Quotas
|
|
692
|
-
Limites de recursos por tenant.
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
export default defineConfig({
|
|
696
|
-
quotas: {
|
|
697
|
-
enabled: true,
|
|
698
|
-
defaults: {
|
|
699
|
-
maxRows: 1000000,
|
|
700
|
-
maxStorageMb: 1024,
|
|
701
|
-
maxTablesRows: {
|
|
702
|
-
users: 10000,
|
|
703
|
-
logs: 100000,
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
perTenant: async (tenantId) => {
|
|
707
|
-
const plan = await getTenantPlan(tenantId);
|
|
708
|
-
return quotasByPlan[plan];
|
|
709
|
-
},
|
|
710
|
-
onQuotaExceeded: async (tenantId, quota, current) => {
|
|
711
|
-
// 'warn' | 'block' | 'notify'
|
|
712
|
-
await notifyTenantAdmin(tenantId, quota);
|
|
713
|
-
return 'warn';
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
});
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
#### Encryption at Rest
|
|
720
|
-
Criptografia de colunas sensíveis.
|
|
721
|
-
|
|
722
|
-
```typescript
|
|
723
|
-
export default defineConfig({
|
|
724
|
-
encryption: {
|
|
725
|
-
enabled: true,
|
|
726
|
-
keyProvider: {
|
|
727
|
-
type: 'aws-kms',
|
|
728
|
-
keyId: process.env.KMS_KEY_ID!,
|
|
729
|
-
region: 'us-east-1',
|
|
730
|
-
},
|
|
731
|
-
// ou
|
|
732
|
-
keyProvider: {
|
|
733
|
-
type: 'vault',
|
|
734
|
-
url: process.env.VAULT_URL!,
|
|
735
|
-
token: process.env.VAULT_TOKEN!,
|
|
736
|
-
},
|
|
737
|
-
columns: [
|
|
738
|
-
'users.ssn',
|
|
739
|
-
'users.tax_id',
|
|
740
|
-
'payments.card_number',
|
|
741
|
-
'payments.cvv',
|
|
742
|
-
],
|
|
743
|
-
},
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
// Transparente para o código
|
|
747
|
-
const user = await db.select().from(users).where(eq(users.id, '123'));
|
|
748
|
-
// user.ssn já descriptografado
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
#### Cross-Tenant Admin Queries
|
|
752
|
-
Queries agregadas para dashboards administrativos.
|
|
753
|
-
|
|
754
|
-
```typescript
|
|
755
|
-
import { createAdminQuery } from 'drizzle-multitenant';
|
|
756
|
-
|
|
757
|
-
const adminQuery = createAdminQuery({ manager });
|
|
758
|
-
|
|
759
|
-
// Agregação cross-tenant
|
|
760
|
-
const stats = await adminQuery
|
|
761
|
-
.fromAllTenants(users)
|
|
762
|
-
.select({
|
|
763
|
-
tenantId: sql`current_schema()`,
|
|
764
|
-
count: count(),
|
|
765
|
-
activeUsers: count(sql`CASE WHEN active THEN 1 END`),
|
|
766
|
-
})
|
|
767
|
-
.groupBy(sql`current_schema()`);
|
|
768
|
-
|
|
769
|
-
// Resultado:
|
|
770
|
-
// [
|
|
771
|
-
// { tenantId: 'tenant_abc', count: 1500, activeUsers: 1200 },
|
|
772
|
-
// { tenantId: 'tenant_def', count: 3000, activeUsers: 2800 },
|
|
773
|
-
// ]
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
**Checklist v2.0.0:**
|
|
777
|
-
- [ ] Multi-region support
|
|
778
|
-
- [ ] Backup/Restore API
|
|
779
|
-
- [ ] S3 integration
|
|
780
|
-
- [ ] Tenant quotas
|
|
781
|
-
- [ ] Column encryption (KMS/Vault)
|
|
782
|
-
- [ ] Cross-tenant admin queries
|
|
783
|
-
- [ ] Breaking changes migration guide
|
|
784
|
-
|
|
785
|
-
---
|
|
786
|
-
|
|
787
|
-
## Priorização
|
|
788
|
-
|
|
789
|
-
| Versão | Tema | Impacto | Esforço | ETA |
|
|
790
|
-
|--------|------|---------|---------|-----|
|
|
791
|
-
| v1.1.0 | Resiliência | Alto | Médio | - |
|
|
792
|
-
| v1.2.0 | Segurança | Alto | Médio | - |
|
|
793
|
-
| v1.3.0 | Performance | Médio | Alto | - |
|
|
794
|
-
| v1.4.0 | Estratégias | Alto | Alto | - |
|
|
795
|
-
| v1.5.0 | DX | Médio | Médio | - |
|
|
796
|
-
| v1.6.0 | Integrações | Médio | Médio | - |
|
|
797
|
-
| v2.0.0 | Enterprise | Alto | Muito Alto | - |
|
|
798
|
-
|
|
799
|
-
---
|
|
800
|
-
|
|
801
|
-
## Quick Wins (Podem entrar em qualquer versão)
|
|
802
|
-
|
|
803
|
-
| Feature | Esforço | Versão | Status |
|
|
804
|
-
|---------|---------|--------|--------|
|
|
805
|
-
| Health check API | 2h | v1.1.0 | Pendente |
|
|
806
|
-
| Schema name sanitization | 1h | v1.2.0 | Pendente |
|
|
807
|
-
| CLI interativo básico | 4h | v1.5.0 | Pendente |
|
|
808
|
-
| Structured logging hook | 2h | v1.1.0 | Pendente |
|
|
809
|
-
| Tenant clone (schema only) | 4h | v1.5.0 | Pendente |
|
|
810
|
-
| ~~CLI migrationsTable config~~ | 1h | v1.0.3 | **Concluído** |
|
|
811
|
-
| ~~TenantDbFactory para singletons~~ | 2h | v1.0.3 | **Concluído** |
|
|
812
|
-
| ~~Debug utilities para proxies~~ | 1h | v1.0.3 | **Concluído** |
|
|
813
|
-
|
|
814
|
-
---
|
|
815
|
-
|
|
816
|
-
## v1.0.4 - CLI migrationsTable Support
|
|
817
|
-
|
|
818
|
-
### Problema
|
|
819
|
-
|
|
820
|
-
A CLI do `drizzle-multitenant` usa a tabela `__drizzle_migrations` para tracking de migrations, mas não permite configurar um nome diferente. Isso causa incompatibilidade com projetos que já usam outra tabela de tracking (ex: `__drizzle_tenant_migrations`).
|
|
821
|
-
|
|
822
|
-
### Solução
|
|
823
|
-
|
|
824
|
-
Ler o campo `migrationsTable` do objeto `migrations` na config e passá-lo para o `Migrator`.
|
|
825
|
-
|
|
826
|
-
### Mudanças Necessárias
|
|
827
|
-
|
|
828
|
-
#### 1. Atualizar `loadConfig` em `src/cli/utils.ts`
|
|
829
|
-
|
|
830
|
-
```typescript
|
|
831
|
-
export async function loadConfig(configPath?: string) {
|
|
832
|
-
// ... código existente ...
|
|
833
|
-
|
|
834
|
-
return {
|
|
835
|
-
config: exported,
|
|
836
|
-
migrationsFolder: exported.migrations?.tenantFolder,
|
|
837
|
-
tenantDiscovery: exported.migrations?.tenantDiscovery,
|
|
838
|
-
migrationsTable: exported.migrations?.migrationsTable, // NOVO
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
#### 2. Atualizar comandos em `src/cli/commands/`
|
|
844
|
-
|
|
845
|
-
Passar `migrationsTable` para o `createMigrator`:
|
|
846
|
-
|
|
847
|
-
```typescript
|
|
848
|
-
// migrate.ts, status.ts, tenant-create.ts, tenant-drop.ts
|
|
849
|
-
const { config, migrationsFolder, tenantDiscovery, migrationsTable } = await loadConfig(options.config);
|
|
850
|
-
|
|
851
|
-
const migrator = createMigrator(config, {
|
|
852
|
-
migrationsFolder: folder,
|
|
853
|
-
tenantDiscovery: discoveryFn,
|
|
854
|
-
migrationsTable, // NOVO - passa undefined se não configurado (usa default)
|
|
855
|
-
});
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
#### 3. Atualizar tipos
|
|
859
|
-
|
|
860
|
-
```typescript
|
|
861
|
-
// types.ts ou onde apropriado
|
|
862
|
-
interface MigrationsConfig {
|
|
863
|
-
tenantFolder: string;
|
|
864
|
-
tenantDiscovery: () => Promise<string[]>;
|
|
865
|
-
migrationsTable?: string; // NOVO
|
|
866
|
-
}
|
|
867
|
-
```
|
|
868
|
-
|
|
869
|
-
### Exemplo de Uso
|
|
870
|
-
|
|
871
|
-
```typescript
|
|
872
|
-
// tenant.config.ts
|
|
873
|
-
export default {
|
|
874
|
-
...config,
|
|
875
|
-
migrations: {
|
|
876
|
-
tenantFolder: "./drizzle/tenant",
|
|
877
|
-
tenantDiscovery: discoverTenants,
|
|
878
|
-
migrationsTable: "__drizzle_tenant_migrations", // NOVO - usa tabela customizada
|
|
879
|
-
},
|
|
880
|
-
};
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
### Compatibilidade
|
|
884
|
-
|
|
885
|
-
- **Backward compatible**: Se não configurado, usa `__drizzle_migrations` (comportamento atual)
|
|
886
|
-
- **Migration path**: Projetos existentes podem:
|
|
887
|
-
1. Configurar a tabela antiga na config
|
|
888
|
-
2. Ou renomear a tabela no banco para o novo padrão
|
|
889
|
-
|
|
890
|
-
### Checklist
|
|
891
|
-
|
|
892
|
-
- [x] Atualizar `loadConfig` para extrair `migrationsTable`
|
|
893
|
-
- [x] Atualizar `migrate` command
|
|
894
|
-
- [x] Atualizar `status` command
|
|
895
|
-
- [x] Atualizar `tenant:create` command
|
|
896
|
-
- [x] Atualizar `tenant:drop` command
|
|
897
|
-
- [x] Adicionar teste unitário
|
|
898
|
-
- [x] Atualizar README com exemplo
|
|
899
|
-
- [x] ~~Publicar v1.0.4~~ (incluído em v1.0.3)
|
|
900
|
-
|
|
901
|
-
---
|
|
902
|
-
|
|
903
|
-
## Breaking Changes Planejados (v2.0.0)
|
|
904
|
-
|
|
905
|
-
1. **Namespace de hooks**: Mover hooks para objeto dedicado
|
|
906
|
-
2. **Métricas opt-out**: Métricas habilitadas por padrão
|
|
907
|
-
3. **Config validation**: Validação mais estrita em runtime
|
|
908
|
-
4. **Import paths**: Consolidar exports
|
|
909
|
-
|
|
910
|
-
```typescript
|
|
911
|
-
// v1.x
|
|
912
|
-
import { createTenantManager } from 'drizzle-multitenant';
|
|
913
|
-
import { createExpressMiddleware } from 'drizzle-multitenant/express';
|
|
914
|
-
|
|
915
|
-
// v2.0 (proposta)
|
|
916
|
-
import {
|
|
917
|
-
createTenantManager,
|
|
918
|
-
createExpressMiddleware,
|
|
919
|
-
createFastifyPlugin,
|
|
920
|
-
} from 'drizzle-multitenant';
|
|
921
|
-
```
|