drizzle-multitenant 1.0.4 → 1.0.6
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/.claude/settings.local.json +5 -1
- package/README.md +112 -8
- package/dist/cli/index.js +1347 -141
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +178 -29
- package/dist/index.js.map +1 -1
- package/dist/migrator/index.d.ts +85 -4
- package/dist/migrator/index.js +197 -30
- package/dist/migrator/index.js.map +1 -1
- package/package.json +4 -1
- package/proposals/improvements-from-primesys.md +385 -0
|
@@ -0,0 +1,385 @@
|
|
|
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)
|