Security fixes (v1.2.2): - Fix SQL injection in analytics.ts (16 occurrences) - Fix SQL injection in advanced-search.ts (1 occurrence) - Fix SQL injection in search-queries.ts (1 occurrence) - Add validateDaysInterval(), isValidISODate(), validatePeriod() to security.ts - Use make_interval(days => N) for safe PostgreSQL intervals - Validate UUIDs BEFORE string construction Transaction support: - bulk-operations.ts: 6 atomic operations with withTransaction() - desk-sync.ts: 2 operations with transactions - export-import.ts: 1 operation with transaction Rate limiting: - Add automatic cleanup of expired entries (every 5 minutes) Audit: - Archive previous audit docs to docs/audits/2026-01-31-v1.2.1/ - Create new AUDIT-REQUEST.md for v1.2.2 verification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1197 lines
27 KiB
Markdown
1197 lines
27 KiB
Markdown
# Plano de Melhorias - MCP Outline PostgreSQL v1.2.1
|
|
|
|
**Data:** 2026-01-31
|
|
**Projecto:** MCP Outline PostgreSQL
|
|
**Versão Actual:** 1.2.1
|
|
**Versão Alvo:** 2.0.0 (Production-Ready)
|
|
|
|
---
|
|
|
|
## 🎯 Objectivo
|
|
|
|
Transformar o MCP Outline PostgreSQL de um protótipo funcional (7.2/10) num servidor production-ready (9.5/10) através de correcções de segurança críticas, optimizações de performance e melhorias de qualidade.
|
|
|
|
---
|
|
|
|
## 📋 Resumo das Melhorias
|
|
|
|
| Fase | Foco | Duração | Prioridade | Tarefas |
|
|
|------|------|---------|------------|---------|
|
|
| **Fase 1** | Segurança Crítica | 2 semanas | 🔴 P0 | 12 |
|
|
| **Fase 2** | Performance | 1 semana | 🟡 P1 | 10 |
|
|
| **Fase 3** | Qualidade | 2 semanas | 🟢 P2 | 15 |
|
|
| **Fase 4** | Funcionalidades | Ongoing | 🟢 P3 | 15 |
|
|
|
|
**Total:** 52 tarefas | 5 semanas de trabalho core
|
|
|
|
---
|
|
|
|
## 🔴 FASE 1: Segurança Crítica (P0)
|
|
|
|
**Duração:** 2 semanas
|
|
**Prioridade:** CRÍTICA
|
|
**Bloqueante:** SIM - Não deploy sem esta fase completa
|
|
|
|
### Objectivos
|
|
|
|
- ✅ Eliminar 100% das vulnerabilidades de SQL Injection
|
|
- ✅ Implementar transacções em todas as operações críticas
|
|
- ✅ Adicionar validação robusta de inputs
|
|
- ✅ Implementar audit logging básico
|
|
|
|
---
|
|
|
|
### Tarefas Detalhadas
|
|
|
|
#### 1.1 Corrigir SQL Injection (Semana 1)
|
|
|
|
**Problema:** 164 tools com potencial SQL injection via string concatenation.
|
|
|
|
**Solução:** Converter todas as queries para prepared statements parametrizados.
|
|
|
|
##### 1.1.1 Auditar e Catalogar Queries Vulneráveis
|
|
|
|
```bash
|
|
# Script para identificar queries vulneráveis
|
|
grep -r "pool.query(\`" src/tools/ > vulnerable-queries.txt
|
|
```
|
|
|
|
**Ficheiros Prioritários:**
|
|
- [ ] `documents.ts` (19 tools) - 2 dias
|
|
- [ ] `collections.ts` (14 tools) - 1.5 dias
|
|
- [ ] `users.ts` (9 tools) - 1 dia
|
|
- [ ] `advanced-search.ts` (6 tools) - 1 dia
|
|
- [ ] `analytics.ts` (6 tools) - 1 dia
|
|
- [ ] Restantes 27 ficheiros - 2 dias
|
|
|
|
**Total:** 8.5 dias
|
|
|
|
##### 1.1.2 Criar Função Helper para Queries Seguras
|
|
|
|
**Ficheiro:** `src/utils/query-builder.ts`
|
|
|
|
```typescript
|
|
/**
|
|
* Query builder seguro com prepared statements
|
|
*/
|
|
export class SafeQueryBuilder {
|
|
private params: any[] = [];
|
|
private paramIndex = 1;
|
|
|
|
/**
|
|
* Adiciona parâmetro e retorna placeholder
|
|
*/
|
|
addParam(value: any): string {
|
|
this.params.push(value);
|
|
return `$${this.paramIndex++}`;
|
|
}
|
|
|
|
/**
|
|
* Constrói WHERE clause com ILIKE seguro
|
|
*/
|
|
buildILike(column: string, value: string): string {
|
|
return `${column} ILIKE ${this.addParam(`%${sanitizeInput(value)}%`)}`;
|
|
}
|
|
|
|
/**
|
|
* Constrói IN clause seguro
|
|
*/
|
|
buildIn(column: string, values: any[]): string {
|
|
const placeholders = values.map(v => this.addParam(v)).join(', ');
|
|
return `${column} IN (${placeholders})`;
|
|
}
|
|
|
|
getParams(): any[] {
|
|
return this.params;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
##### 1.1.3 Refactoring de Queries
|
|
|
|
**Antes (VULNERÁVEL):**
|
|
```typescript
|
|
const query = `
|
|
SELECT * FROM documents
|
|
WHERE title ILIKE '%${args.query}%'
|
|
AND collectionId = '${args.collection_id}'
|
|
`;
|
|
const result = await pool.query(query);
|
|
```
|
|
|
|
**Depois (SEGURO):**
|
|
```typescript
|
|
const qb = new SafeQueryBuilder();
|
|
const query = `
|
|
SELECT * FROM documents
|
|
WHERE title ILIKE ${qb.addParam(`%${sanitizeInput(args.query)}%`)}
|
|
AND collectionId = ${qb.addParam(args.collection_id)}
|
|
`;
|
|
const result = await pool.query(query, qb.getParams());
|
|
```
|
|
|
|
**Estimativa:** 8 dias (todos os ficheiros)
|
|
|
|
##### 1.1.4 Adicionar Linting Rule
|
|
|
|
**Ficheiro:** `.eslintrc.json`
|
|
|
|
```json
|
|
{
|
|
"rules": {
|
|
"no-template-curly-in-string": "error",
|
|
"no-restricted-syntax": [
|
|
"error",
|
|
{
|
|
"selector": "TemplateLiteral[parent.callee.property.name='query']",
|
|
"message": "Use parameterized queries to prevent SQL injection"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
---
|
|
|
|
#### 1.2 Implementar Transacções (Semana 2)
|
|
|
|
**Problema:** Operações multi-write sem atomicidade.
|
|
|
|
**Solução:** Envolver operações críticas em transacções.
|
|
|
|
##### 1.2.1 Identificar Operações Críticas
|
|
|
|
**Ficheiros Afectados:**
|
|
- [ ] `bulk-operations.ts` - 6 tools
|
|
- [ ] `desk-sync.ts` - 2 tools
|
|
- [ ] `export-import.ts` - 2 tools
|
|
- [ ] `collections.ts` - memberships (4 tools)
|
|
- [ ] `documents.ts` - create/update com memberships (2 tools)
|
|
|
|
**Total:** 16 tools
|
|
|
|
##### 1.2.2 Criar Transaction Helper
|
|
|
|
**Ficheiro:** `src/utils/transaction.ts`
|
|
|
|
```typescript
|
|
import { Pool, PoolClient } from 'pg';
|
|
|
|
/**
|
|
* Executa operação em transacção com retry automático
|
|
*/
|
|
export async function withTransaction<T>(
|
|
pool: Pool,
|
|
callback: (client: PoolClient) => Promise<T>,
|
|
maxRetries = 3
|
|
): Promise<T> {
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
const result = await callback(client);
|
|
await client.query('COMMIT');
|
|
return result;
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
lastError = error as Error;
|
|
|
|
// Retry apenas em deadlocks
|
|
if (error instanceof Error && error.message.includes('deadlock')) {
|
|
await new Promise(resolve => setTimeout(resolve, 100 * attempt));
|
|
continue;
|
|
}
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
throw lastError!;
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
##### 1.2.3 Refactoring de Operações Bulk
|
|
|
|
**Antes (SEM TRANSACÇÃO):**
|
|
```typescript
|
|
handler: async (args, pgClient) => {
|
|
const pool = pgClient.getPool();
|
|
for (const id of args.document_ids) {
|
|
await pool.query('UPDATE documents SET archivedAt = NOW() WHERE id = $1', [id]);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Depois (COM TRANSACÇÃO):**
|
|
```typescript
|
|
handler: async (args, pgClient) => {
|
|
const pool = pgClient.getPool();
|
|
|
|
return await withTransaction(pool, async (client) => {
|
|
const results = [];
|
|
for (const id of args.document_ids) {
|
|
const result = await client.query(
|
|
'UPDATE documents SET archivedAt = NOW() WHERE id = $1 RETURNING *',
|
|
[id]
|
|
);
|
|
results.push(result.rows[0]);
|
|
}
|
|
return results;
|
|
});
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 2 dias (16 tools)
|
|
|
|
##### 1.2.4 Testes de Rollback
|
|
|
|
**Ficheiro:** `tests/transactions.test.ts`
|
|
|
|
```typescript
|
|
describe('Transaction Rollback', () => {
|
|
it('should rollback on error in bulk operations', async () => {
|
|
const invalidIds = ['valid-uuid', 'INVALID'];
|
|
|
|
await expect(
|
|
bulkArchiveDocuments({ document_ids: invalidIds }, pgClient)
|
|
).rejects.toThrow();
|
|
|
|
// Verificar que nenhum documento foi arquivado
|
|
const result = await pool.query(
|
|
'SELECT * FROM documents WHERE archivedAt IS NOT NULL'
|
|
);
|
|
expect(result.rows).toHaveLength(0);
|
|
});
|
|
});
|
|
```
|
|
|
|
**Estimativa:** 1 dia
|
|
|
|
---
|
|
|
|
#### 1.3 Validação de Inputs (Semana 2)
|
|
|
|
##### 1.3.1 Implementar Validação Automática
|
|
|
|
**Ficheiro:** `src/utils/validation.ts`
|
|
|
|
```typescript
|
|
import Ajv from 'ajv';
|
|
import addFormats from 'ajv-formats';
|
|
|
|
const ajv = new Ajv({ allErrors: true });
|
|
addFormats(ajv);
|
|
|
|
/**
|
|
* Valida args contra inputSchema
|
|
*/
|
|
export function validateToolInput<T>(
|
|
args: unknown,
|
|
schema: object
|
|
): T {
|
|
const validate = ajv.compile(schema);
|
|
|
|
if (!validate(args)) {
|
|
const errors = validate.errors?.map(e => `${e.instancePath} ${e.message}`).join(', ');
|
|
throw new Error(`Invalid input: ${errors}`);
|
|
}
|
|
|
|
return args as T;
|
|
}
|
|
|
|
/**
|
|
* Middleware para validação automática
|
|
*/
|
|
export function withValidation<T>(
|
|
tool: BaseTool<T>
|
|
): BaseTool<T> {
|
|
const originalHandler = tool.handler;
|
|
|
|
return {
|
|
...tool,
|
|
handler: async (args, pgClient) => {
|
|
const validatedArgs = validateToolInput<T>(args, tool.inputSchema);
|
|
return originalHandler(validatedArgs, pgClient);
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 1 dia
|
|
|
|
##### 1.3.2 Adicionar Validações Específicas
|
|
|
|
**Melhorias em `src/utils/security.ts`:**
|
|
|
|
```typescript
|
|
/**
|
|
* Valida array de UUIDs
|
|
*/
|
|
export function validateUUIDs(uuids: string[]): void {
|
|
const invalid = uuids.filter(uuid => !isValidUUID(uuid));
|
|
if (invalid.length > 0) {
|
|
throw new Error(`Invalid UUIDs: ${invalid.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Valida enum value
|
|
*/
|
|
export function validateEnum<T>(
|
|
value: string,
|
|
allowedValues: T[],
|
|
fieldName: string
|
|
): T {
|
|
if (!allowedValues.includes(value as T)) {
|
|
throw new Error(
|
|
`Invalid ${fieldName}: ${value}. Allowed: ${allowedValues.join(', ')}`
|
|
);
|
|
}
|
|
return value as T;
|
|
}
|
|
|
|
/**
|
|
* Valida tamanho de string
|
|
*/
|
|
export function validateStringLength(
|
|
value: string,
|
|
min: number,
|
|
max: number,
|
|
fieldName: string
|
|
): void {
|
|
if (value.length < min || value.length > max) {
|
|
throw new Error(
|
|
`${fieldName} must be between ${min} and ${max} characters`
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
---
|
|
|
|
#### 1.4 Audit Logging Básico (Semana 2)
|
|
|
|
##### 1.4.1 Criar Sistema de Audit Log
|
|
|
|
**Ficheiro:** `src/utils/audit.ts`
|
|
|
|
```typescript
|
|
import { Pool } from 'pg';
|
|
|
|
export interface AuditLogEntry {
|
|
userId?: string;
|
|
action: string;
|
|
resourceType: string;
|
|
resourceId: string;
|
|
metadata?: Record<string, any>;
|
|
}
|
|
|
|
/**
|
|
* Regista operação em audit log
|
|
*/
|
|
export async function logAudit(
|
|
pool: Pool,
|
|
entry: AuditLogEntry
|
|
): Promise<void> {
|
|
await pool.query(
|
|
`INSERT INTO events (name, actorId, modelId, data, createdAt)
|
|
VALUES ($1, $2, $3, $4, NOW())`,
|
|
[
|
|
entry.action,
|
|
entry.userId || null,
|
|
entry.resourceId,
|
|
JSON.stringify({
|
|
resourceType: entry.resourceType,
|
|
...entry.metadata
|
|
})
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Middleware para audit logging automático
|
|
*/
|
|
export function withAuditLog<T>(
|
|
tool: BaseTool<T>,
|
|
getResourceInfo: (args: T) => { type: string; id: string }
|
|
): BaseTool<T> {
|
|
const originalHandler = tool.handler;
|
|
|
|
return {
|
|
...tool,
|
|
handler: async (args, pgClient) => {
|
|
const result = await originalHandler(args, pgClient);
|
|
|
|
// Log apenas operações de escrita
|
|
if (['create', 'update', 'delete'].some(op => tool.name.includes(op))) {
|
|
const resource = getResourceInfo(args);
|
|
await logAudit(pgClient.getPool(), {
|
|
action: tool.name,
|
|
resourceType: resource.type,
|
|
resourceId: resource.id
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 1 dia
|
|
|
|
---
|
|
|
|
### Checklist Fase 1
|
|
|
|
- [ ] **SQL Injection**
|
|
- [ ] Auditar 164 tools
|
|
- [ ] Criar SafeQueryBuilder
|
|
- [ ] Refactoring completo
|
|
- [ ] Adicionar linting rule
|
|
- [ ] Testar manualmente cada módulo
|
|
|
|
- [ ] **Transacções**
|
|
- [ ] Identificar operações críticas
|
|
- [ ] Criar transaction helper
|
|
- [ ] Refactoring bulk operations
|
|
- [ ] Implementar testes de rollback
|
|
|
|
- [ ] **Validação**
|
|
- [ ] Implementar validação automática
|
|
- [ ] Adicionar validações específicas
|
|
- [ ] Aplicar a todos os tools
|
|
|
|
- [ ] **Audit Log**
|
|
- [ ] Criar sistema de logging
|
|
- [ ] Integrar com tools de escrita
|
|
- [ ] Testar logging
|
|
|
|
### Métricas de Sucesso Fase 1
|
|
|
|
- ✅ 0 queries com string concatenation
|
|
- ✅ 100% queries parametrizadas
|
|
- ✅ 16 operações críticas com transacções
|
|
- ✅ 100% tools com validação de input
|
|
- ✅ Audit log funcional em operações de escrita
|
|
|
|
---
|
|
|
|
## 🟡 FASE 2: Performance (P1)
|
|
|
|
**Duração:** 1 semana
|
|
**Prioridade:** ALTA
|
|
**Bloqueante:** NÃO - Mas recomendado antes de produção
|
|
|
|
### Objectivos
|
|
|
|
- ✅ Eliminar N+1 queries
|
|
- ✅ Criar índices necessários
|
|
- ✅ Optimizar connection pool
|
|
- ✅ Implementar cursor-based pagination
|
|
|
|
---
|
|
|
|
### Tarefas Detalhadas
|
|
|
|
#### 2.1 Eliminar N+1 Queries (2 dias)
|
|
|
|
##### 2.1.1 Identificar N+1 Queries
|
|
|
|
**Ficheiros Afectados:**
|
|
- `collections.ts:1253-1280` - export_all_collections
|
|
- `documents.ts:530-577` - list_drafts
|
|
- `analytics.ts` - várias queries
|
|
|
|
##### 2.1.2 Refactoring com JOINs
|
|
|
|
**Antes (N+1):**
|
|
```typescript
|
|
const collections = await pool.query('SELECT * FROM collections');
|
|
for (const collection of collections.rows) {
|
|
const docs = await pool.query(
|
|
'SELECT * FROM documents WHERE collectionId = $1',
|
|
[collection.id]
|
|
);
|
|
collection.documents = docs.rows;
|
|
}
|
|
```
|
|
|
|
**Depois (JOIN):**
|
|
```typescript
|
|
const result = await pool.query(`
|
|
SELECT
|
|
c.*,
|
|
json_agg(
|
|
json_build_object(
|
|
'id', d.id,
|
|
'title', d.title,
|
|
'createdAt', d.createdAt
|
|
)
|
|
) FILTER (WHERE d.id IS NOT NULL) as documents
|
|
FROM collections c
|
|
LEFT JOIN documents d ON d.collectionId = c.id AND d.deletedAt IS NULL
|
|
GROUP BY c.id
|
|
`);
|
|
```
|
|
|
|
**Estimativa:** 2 dias
|
|
|
|
---
|
|
|
|
#### 2.2 Criar Índices (1 dia)
|
|
|
|
##### 2.2.1 Documentar Índices Necessários
|
|
|
|
**Ficheiro:** `migrations/001_indexes.sql`
|
|
|
|
```sql
|
|
-- Full-text search
|
|
CREATE INDEX IF NOT EXISTS idx_documents_search
|
|
ON documents USING gin(to_tsvector('english', title || ' ' || COALESCE(text, '')));
|
|
|
|
-- Queries comuns
|
|
CREATE INDEX IF NOT EXISTS idx_documents_collection_id
|
|
ON documents(collectionId) WHERE deletedAt IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_documents_published
|
|
ON documents(publishedAt DESC) WHERE deletedAt IS NULL AND publishedAt IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_documents_created
|
|
ON documents(createdAt DESC) WHERE deletedAt IS NULL;
|
|
|
|
-- Memberships
|
|
CREATE INDEX IF NOT EXISTS idx_collection_memberships_lookup
|
|
ON collection_memberships(collectionId, userId);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_group_memberships_lookup
|
|
ON group_memberships(groupId, userId);
|
|
|
|
-- Stars, Pins, Views
|
|
CREATE INDEX IF NOT EXISTS idx_stars_user_document
|
|
ON stars(userId, documentId) WHERE deletedAt IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_pins_collection_document
|
|
ON pins(collectionId, documentId) WHERE deletedAt IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_views_document_user
|
|
ON views(documentId, userId, createdAt DESC);
|
|
|
|
-- Events (audit log)
|
|
CREATE INDEX IF NOT EXISTS idx_events_actor_created
|
|
ON events(actorId, createdAt DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_events_model_created
|
|
ON events(modelId, createdAt DESC);
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
##### 2.2.2 Documentar em SPEC
|
|
|
|
**Adicionar secção em `SPEC-MCP-OUTLINE.md`:**
|
|
|
|
```markdown
|
|
## Índices Recomendados
|
|
|
|
Para performance optimal, execute as migrations em `migrations/001_indexes.sql`.
|
|
|
|
### Índices Críticos
|
|
|
|
| Índice | Tabela | Tipo | Impacto |
|
|
|--------|--------|------|---------|
|
|
| idx_documents_search | documents | GIN | Full-text search 10-100x faster |
|
|
| idx_documents_collection_id | documents | B-tree | List documents 5-10x faster |
|
|
| idx_collection_memberships_lookup | collection_memberships | B-tree | Permission checks 10x faster |
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
---
|
|
|
|
#### 2.3 Optimizar Connection Pool (1 dia)
|
|
|
|
##### 2.3.1 Tuning de Pool
|
|
|
|
**Ficheiro:** `src/config/database.ts`
|
|
|
|
```typescript
|
|
export interface DatabaseConfig {
|
|
// ... existing fields
|
|
|
|
// Pool tuning
|
|
max?: number; // Default: 20
|
|
min?: number; // Default: 5
|
|
idleTimeoutMillis?: number; // Default: 30000
|
|
connectionTimeoutMillis?: number; // Default: 5000
|
|
maxUses?: number; // Default: 7500 (recycle connections)
|
|
|
|
// Performance
|
|
statementTimeout?: number; // Default: 30000 (30s)
|
|
queryTimeout?: number; // Default: 10000 (10s)
|
|
}
|
|
|
|
export function getDefaultConfig(): DatabaseConfig {
|
|
return {
|
|
max: parseInt(process.env.DB_POOL_MAX || '20', 10),
|
|
min: parseInt(process.env.DB_POOL_MIN || '5', 10),
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 5000,
|
|
maxUses: 7500,
|
|
statementTimeout: 30000,
|
|
queryTimeout: 10000
|
|
};
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
##### 2.3.2 Adicionar Pool Monitoring
|
|
|
|
**Ficheiro:** `src/utils/monitoring.ts`
|
|
|
|
```typescript
|
|
import { Pool } from 'pg';
|
|
|
|
export function monitorPool(pool: Pool): void {
|
|
setInterval(() => {
|
|
logger.info('Pool stats', {
|
|
total: pool.totalCount,
|
|
idle: pool.idleCount,
|
|
waiting: pool.waitingCount
|
|
});
|
|
|
|
// Alert se pool saturado
|
|
if (pool.waitingCount > 5) {
|
|
logger.warn('Pool saturation detected', {
|
|
waiting: pool.waitingCount,
|
|
total: pool.totalCount
|
|
});
|
|
}
|
|
}, 60000); // A cada minuto
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
---
|
|
|
|
#### 2.4 Cursor-Based Pagination (1 dia)
|
|
|
|
##### 2.4.1 Implementar Cursor Pagination
|
|
|
|
**Ficheiro:** `src/utils/pagination.ts`
|
|
|
|
```typescript
|
|
export interface CursorPaginationArgs {
|
|
limit?: number;
|
|
cursor?: string; // Base64 encoded timestamp ou ID
|
|
}
|
|
|
|
export interface CursorPaginationResult<T> {
|
|
items: T[];
|
|
nextCursor?: string;
|
|
hasMore: boolean;
|
|
}
|
|
|
|
/**
|
|
* Cursor-based pagination (mais eficiente que OFFSET)
|
|
*/
|
|
export async function paginateWithCursor<T>(
|
|
pool: Pool,
|
|
baseQuery: string,
|
|
cursorField: string,
|
|
args: CursorPaginationArgs
|
|
): Promise<CursorPaginationResult<T>> {
|
|
const limit = Math.min(args.limit || 25, 100);
|
|
|
|
let query = baseQuery;
|
|
const params: any[] = [limit + 1]; // +1 para detectar hasMore
|
|
|
|
if (args.cursor) {
|
|
const cursorValue = Buffer.from(args.cursor, 'base64').toString();
|
|
query += ` AND ${cursorField} < $2`;
|
|
params.push(cursorValue);
|
|
}
|
|
|
|
query += ` ORDER BY ${cursorField} DESC LIMIT $1`;
|
|
|
|
const result = await pool.query<T>(query, params);
|
|
const hasMore = result.rows.length > limit;
|
|
const items = hasMore ? result.rows.slice(0, -1) : result.rows;
|
|
|
|
const nextCursor = hasMore && items.length > 0
|
|
? Buffer.from(String((items[items.length - 1] as any)[cursorField])).toString('base64')
|
|
: undefined;
|
|
|
|
return { items, nextCursor, hasMore };
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 1 dia
|
|
|
|
---
|
|
|
|
### Checklist Fase 2
|
|
|
|
- [ ] **N+1 Queries**
|
|
- [ ] Identificar todas as ocorrências
|
|
- [ ] Refactoring com JOINs
|
|
- [ ] Testar performance
|
|
|
|
- [ ] **Índices**
|
|
- [ ] Criar migrations/001_indexes.sql
|
|
- [ ] Documentar em SPEC
|
|
- [ ] Testar impacto
|
|
|
|
- [ ] **Connection Pool**
|
|
- [ ] Tuning de configuração
|
|
- [ ] Adicionar monitoring
|
|
- [ ] Testar sob carga
|
|
|
|
- [ ] **Pagination**
|
|
- [ ] Implementar cursor-based
|
|
- [ ] Migrar tools principais
|
|
- [ ] Benchmark vs OFFSET
|
|
|
|
### Métricas de Sucesso Fase 2
|
|
|
|
- ✅ 0 N+1 queries em hot paths
|
|
- ✅ Queries < 100ms (p95)
|
|
- ✅ Índices criados e documentados
|
|
- ✅ Pool utilization < 80%
|
|
- ✅ Cursor pagination em listagens principais
|
|
|
|
---
|
|
|
|
## 🟢 FASE 3: Qualidade (P2)
|
|
|
|
**Duração:** 2 semanas
|
|
**Prioridade:** MÉDIA
|
|
**Bloqueante:** NÃO - Melhoria contínua
|
|
|
|
### Objectivos
|
|
|
|
- ✅ Implementar testes unitários
|
|
- ✅ Adicionar testes de integração
|
|
- ✅ Configurar CI/CD
|
|
- ✅ Refactoring de código duplicado
|
|
- ✅ Melhorar documentação
|
|
|
|
---
|
|
|
|
### Tarefas Detalhadas
|
|
|
|
#### 3.1 Testes Unitários (1 semana)
|
|
|
|
##### 3.1.1 Setup de Testing
|
|
|
|
**Ficheiro:** `package.json`
|
|
|
|
```json
|
|
{
|
|
"devDependencies": {
|
|
"vitest": "^1.0.0",
|
|
"@vitest/coverage-v8": "^1.0.0",
|
|
"testcontainers": "^10.0.0"
|
|
},
|
|
"scripts": {
|
|
"test": "vitest",
|
|
"test:coverage": "vitest --coverage",
|
|
"test:ui": "vitest --ui"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Ficheiro:** `vitest.config.ts`
|
|
|
|
```typescript
|
|
import { defineConfig } from 'vitest/config';
|
|
|
|
export default defineConfig({
|
|
test: {
|
|
globals: true,
|
|
environment: 'node',
|
|
coverage: {
|
|
provider: 'v8',
|
|
reporter: ['text', 'json', 'html'],
|
|
exclude: ['dist/', 'tests/', '**/*.test.ts']
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
**Estimativa:** 0.5 dias
|
|
|
|
##### 3.1.2 Testes de Utils
|
|
|
|
**Ficheiro:** `tests/utils/security.test.ts`
|
|
|
|
```typescript
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
isValidUUID,
|
|
isValidEmail,
|
|
sanitizeInput,
|
|
validatePagination
|
|
} from '../../src/utils/security';
|
|
|
|
describe('Security Utils', () => {
|
|
describe('isValidUUID', () => {
|
|
it('should validate correct UUIDs', () => {
|
|
expect(isValidUUID('123e4567-e89b-12d3-a456-426614174000')).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid UUIDs', () => {
|
|
expect(isValidUUID('not-a-uuid')).toBe(false);
|
|
expect(isValidUUID('')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('sanitizeInput', () => {
|
|
it('should remove null bytes', () => {
|
|
expect(sanitizeInput('test\0data')).toBe('testdata');
|
|
});
|
|
|
|
it('should trim whitespace', () => {
|
|
expect(sanitizeInput(' test ')).toBe('test');
|
|
});
|
|
});
|
|
|
|
describe('validatePagination', () => {
|
|
it('should enforce max limit', () => {
|
|
const result = validatePagination(1000, 0);
|
|
expect(result.limit).toBe(100);
|
|
});
|
|
|
|
it('should use defaults', () => {
|
|
const result = validatePagination();
|
|
expect(result.limit).toBe(25);
|
|
expect(result.offset).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
**Estimativa:** 2 dias (todos os utils)
|
|
|
|
##### 3.1.3 Testes de Tools
|
|
|
|
**Ficheiro:** `tests/tools/documents.test.ts`
|
|
|
|
```typescript
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { GenericContainer, StartedTestContainer } from 'testcontainers';
|
|
import { Pool } from 'pg';
|
|
import { documentsTools } from '../../src/tools/documents';
|
|
|
|
describe('Documents Tools', () => {
|
|
let container: StartedTestContainer;
|
|
let pool: Pool;
|
|
|
|
beforeAll(async () => {
|
|
// Start PostgreSQL container
|
|
container = await new GenericContainer('postgres:15')
|
|
.withEnvironment({ POSTGRES_PASSWORD: 'test' })
|
|
.withExposedPorts(5432)
|
|
.start();
|
|
|
|
pool = new Pool({
|
|
host: container.getHost(),
|
|
port: container.getMappedPort(5432),
|
|
user: 'postgres',
|
|
password: 'test',
|
|
database: 'postgres'
|
|
});
|
|
|
|
// Setup schema
|
|
await pool.query(/* schema SQL */);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await pool.end();
|
|
await container.stop();
|
|
});
|
|
|
|
it('should list documents', async () => {
|
|
const result = await documentsTools[0].handler({}, { getPool: () => pool });
|
|
expect(result.content[0].text).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
**Estimativa:** 3 dias (tools principais)
|
|
|
|
---
|
|
|
|
#### 3.2 CI/CD (2 dias)
|
|
|
|
##### 3.2.1 GitHub Actions
|
|
|
|
**Ficheiro:** `.github/workflows/ci.yml`
|
|
|
|
```yaml
|
|
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main, develop]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:15
|
|
env:
|
|
POSTGRES_PASSWORD: test
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
cache: 'npm'
|
|
|
|
- run: npm ci
|
|
- run: npm run build
|
|
- run: npm run test:coverage
|
|
|
|
- name: Upload coverage
|
|
uses: codecov/codecov-action@v3
|
|
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
- run: npm ci
|
|
- run: npm run lint
|
|
```
|
|
|
|
**Estimativa:** 1 dia
|
|
|
|
---
|
|
|
|
#### 3.3 Refactoring (3 dias)
|
|
|
|
##### 3.3.1 Eliminar Duplicação
|
|
|
|
**Criar:** `src/utils/tool-factory.ts`
|
|
|
|
```typescript
|
|
/**
|
|
* Factory para criar tools com padrão consistente
|
|
*/
|
|
export function createTool<T>(config: {
|
|
name: string;
|
|
description: string;
|
|
inputSchema: object;
|
|
handler: (args: T, pool: Pool) => Promise<any>;
|
|
requiresTransaction?: boolean;
|
|
auditLog?: boolean;
|
|
}): BaseTool<T> {
|
|
return {
|
|
name: config.name,
|
|
description: config.description,
|
|
inputSchema: config.inputSchema,
|
|
handler: async (args, pgClient) => {
|
|
try {
|
|
// Validação automática
|
|
const validatedArgs = validateToolInput<T>(args, config.inputSchema);
|
|
|
|
const pool = pgClient.getPool();
|
|
|
|
// Executar com ou sem transacção
|
|
const result = config.requiresTransaction
|
|
? await withTransaction(pool, client => config.handler(validatedArgs, client))
|
|
: await config.handler(validatedArgs, pool);
|
|
|
|
// Audit log automático
|
|
if (config.auditLog) {
|
|
await logAudit(pool, {
|
|
action: config.name,
|
|
resourceType: extractResourceType(config.name),
|
|
resourceId: extractResourceId(result)
|
|
});
|
|
}
|
|
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify(result, null, 2)
|
|
}]
|
|
};
|
|
} catch (error) {
|
|
logger.error(`Tool ${config.name} failed`, { error });
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
**Estimativa:** 2 dias
|
|
|
|
##### 3.3.2 Aplicar Factory
|
|
|
|
**Refactoring de tools para usar factory:**
|
|
|
|
```typescript
|
|
// Antes
|
|
const listDocuments: BaseTool<DocumentArgs> = {
|
|
name: 'list_documents',
|
|
description: '...',
|
|
inputSchema: { /* ... */ },
|
|
handler: async (args, pgClient) => {
|
|
try {
|
|
const pool = pgClient.getPool();
|
|
// ... lógica
|
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
} catch (error) {
|
|
// ...
|
|
}
|
|
}
|
|
};
|
|
|
|
// Depois
|
|
const listDocuments = createTool<DocumentArgs>({
|
|
name: 'list_documents',
|
|
description: '...',
|
|
inputSchema: { /* ... */ },
|
|
handler: async (args, pool) => {
|
|
// ... apenas lógica de negócio
|
|
return result;
|
|
}
|
|
});
|
|
```
|
|
|
|
**Estimativa:** 1 dia
|
|
|
|
---
|
|
|
|
### Checklist Fase 3
|
|
|
|
- [ ] **Testes**
|
|
- [ ] Setup Vitest + Testcontainers
|
|
- [ ] Testes de utils (100% coverage)
|
|
- [ ] Testes de tools principais (>80% coverage)
|
|
- [ ] Testes de integração
|
|
|
|
- [ ] **CI/CD**
|
|
- [ ] GitHub Actions
|
|
- [ ] Codecov integration
|
|
- [ ] Automated releases
|
|
|
|
- [ ] **Refactoring**
|
|
- [ ] Tool factory
|
|
- [ ] Eliminar duplicação
|
|
- [ ] Melhorar type safety
|
|
|
|
### Métricas de Sucesso Fase 3
|
|
|
|
- ✅ Code coverage > 80%
|
|
- ✅ 0 linting errors
|
|
- ✅ CI passing
|
|
- ✅ Duplicação < 5%
|
|
|
|
---
|
|
|
|
## 🟢 FASE 4: Funcionalidades (P3)
|
|
|
|
**Duração:** Ongoing
|
|
**Prioridade:** BAIXA
|
|
**Bloqueante:** NÃO - Melhorias incrementais
|
|
|
|
### Tarefas
|
|
|
|
#### 4.1 Rate Limiting Distribuído
|
|
- [ ] Integrar Redis
|
|
- [ ] Implementar rate limiting distribuído
|
|
- [ ] Adicionar CAPTCHA para operações sensíveis
|
|
|
|
#### 4.2 Autorização
|
|
- [ ] Implementar RBAC
|
|
- [ ] Verificar permissões antes de operações
|
|
- [ ] Adicionar testes de autorização
|
|
|
|
#### 4.3 Monitoring
|
|
- [ ] Integrar Prometheus
|
|
- [ ] Adicionar métricas de performance
|
|
- [ ] Dashboard Grafana
|
|
|
|
#### 4.4 Documentação
|
|
- [ ] API documentation (OpenAPI)
|
|
- [ ] Guia de deployment
|
|
- [ ] Troubleshooting guide
|
|
|
|
---
|
|
|
|
## 📊 Métricas Globais de Sucesso
|
|
|
|
### Segurança
|
|
- ✅ 0 vulnerabilidades críticas
|
|
- ✅ 0 vulnerabilidades altas
|
|
- ✅ 100% queries parametrizadas
|
|
- ✅ 100% operações críticas com transacções
|
|
|
|
### Performance
|
|
- ✅ Queries < 100ms (p95)
|
|
- ✅ Throughput > 1000 req/s
|
|
- ✅ Pool utilization < 80%
|
|
- ✅ 0 N+1 queries
|
|
|
|
### Qualidade
|
|
- ✅ Code coverage > 80%
|
|
- ✅ 0 linting errors
|
|
- ✅ CI passing
|
|
- ✅ Duplicação < 5%
|
|
|
|
---
|
|
|
|
## 📅 Timeline
|
|
|
|
```
|
|
Semana 1: SQL Injection
|
|
Semana 2: Transacções + Validação + Audit
|
|
Semana 3: Performance (N+1, Índices, Pool)
|
|
Semana 4: Testes Unitários
|
|
Semana 5: CI/CD + Refactoring
|
|
Semana 6+: Funcionalidades (ongoing)
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Próximos Passos Imediatos
|
|
|
|
1. **Aprovar este plano** ✅
|
|
2. **Criar branch `security-fixes`**
|
|
3. **Iniciar Fase 1.1.1: Auditar queries vulneráveis**
|
|
4. **Daily progress tracking em `task.md`**
|
|
|
|
---
|
|
|
|
*Plano criado em 2026-01-31 | MCP Outline PostgreSQL v1.2.1 → v2.0.0*
|