Nuevas Épicas (MCH-029 a MCH-033): - Infraestructura SaaS multi-tenant - Auth Social (OAuth2) - Auditoría Empresarial - Feature Flags - Onboarding Wizard Nuevas Integraciones (INT-010 a INT-014): - Email Providers (SendGrid, Mailgun, SES) - Storage Cloud (S3, GCS, Azure) - OAuth Social - Redis Cache - Webhooks Outbound Nuevos ADRs (0004 a 0011): - Notifications Realtime - Feature Flags Strategy - Storage Abstraction - Webhook Retry Strategy - Audit Log Retention - Rate Limiting - OAuth Social Implementation - Email Multi-provider Actualizados: - MASTER_INVENTORY.yml - CONTEXT-MAP.yml - HERENCIA-SIMCO.md - Mapas de documentación Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
210 lines
4.5 KiB
Markdown
210 lines
4.5 KiB
Markdown
---
|
|
id: ADR-0006
|
|
type: ADR
|
|
title: "Storage Abstraction"
|
|
status: Accepted
|
|
decision_date: 2026-01-10
|
|
updated_at: 2026-01-10
|
|
simco_version: "4.0.1"
|
|
stakeholders:
|
|
- "Equipo MiChangarrito"
|
|
tags:
|
|
- storage
|
|
- s3
|
|
- r2
|
|
- abstraction
|
|
- multi-cloud
|
|
---
|
|
|
|
# ADR-0006: Storage Abstraction
|
|
|
|
## Metadata
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | ADR-0006 |
|
|
| **Estado** | Accepted |
|
|
| **Fecha** | 2026-01-10 |
|
|
| **Autor** | Architecture Team |
|
|
| **Supersede** | - |
|
|
|
|
---
|
|
|
|
## Contexto
|
|
|
|
MiChangarrito necesita almacenar archivos (imagenes de productos, facturas, documentos) en la nube. Queremos:
|
|
|
|
1. Flexibilidad para cambiar de proveedor
|
|
2. Desarrollo local sin depender de cloud
|
|
3. Optimizacion de costos (R2 es mas barato que S3)
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
**Implementamos una capa de abstraccion con Factory Pattern que soporta S3, Cloudflare R2 y MinIO.**
|
|
|
|
Todos los proveedores implementan la misma interfaz, permitiendo cambiar de proveedor sin modificar codigo de negocio.
|
|
|
|
```typescript
|
|
interface StorageProvider {
|
|
upload(key: string, file: Buffer, options?: UploadOptions): Promise<UploadResult>;
|
|
download(key: string): Promise<Buffer>;
|
|
delete(key: string): Promise<void>;
|
|
getSignedUrl(key: string, expiresIn: number): Promise<string>;
|
|
list(prefix: string): Promise<FileInfo[]>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Alternativas Consideradas
|
|
|
|
### Opcion 1: Usar S3 directamente
|
|
- **Pros:**
|
|
- Simple
|
|
- Bien documentado
|
|
- **Cons:**
|
|
- Vendor lock-in
|
|
- Sin desarrollo local facil
|
|
- Costos pueden ser altos
|
|
|
|
### Opcion 2: Abstraccion con Factory (Elegida)
|
|
- **Pros:**
|
|
- Flexibilidad de proveedor
|
|
- MinIO para desarrollo local
|
|
- Optimizacion de costos con R2
|
|
- Codigo limpio
|
|
- **Cons:**
|
|
- Complejidad adicional
|
|
- Mantener multiples providers
|
|
|
|
### Opcion 3: Usar libreria existente (flydrive)
|
|
- **Pros:**
|
|
- Ya implementado
|
|
- Multiples drivers
|
|
- **Cons:**
|
|
- Dependencia externa
|
|
- Menos control
|
|
- Puede no cubrir todos nuestros casos
|
|
|
|
---
|
|
|
|
## Consecuencias
|
|
|
|
### Positivas
|
|
|
|
1. **Flexibilidad:** Cambiar proveedor sin impacto en negocio
|
|
2. **Desarrollo:** MinIO local sin credenciales cloud
|
|
3. **Costos:** Migrar a R2 reduce costos ~75%
|
|
4. **Testing:** Facil mock de storage
|
|
|
|
### Negativas
|
|
|
|
1. **Complejidad:** Mantener 3 implementaciones
|
|
2. **Features especificos:** Podemos perder algunas features unicas
|
|
|
|
---
|
|
|
|
## Implementacion
|
|
|
|
### Factory
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class StorageFactory {
|
|
create(provider: StorageProviderType): StorageProvider {
|
|
switch (provider) {
|
|
case 's3':
|
|
return new S3StorageProvider(this.configService);
|
|
case 'r2':
|
|
return new R2StorageProvider(this.configService);
|
|
case 'minio':
|
|
return new MinIOStorageProvider(this.configService);
|
|
default:
|
|
throw new Error(`Unknown storage provider: ${provider}`);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Provider S3
|
|
|
|
```typescript
|
|
class S3StorageProvider implements StorageProvider {
|
|
private client: S3Client;
|
|
|
|
constructor(config: ConfigService) {
|
|
this.client = new S3Client({
|
|
region: config.get('S3_REGION'),
|
|
credentials: {
|
|
accessKeyId: config.get('S3_ACCESS_KEY'),
|
|
secretAccessKey: config.get('S3_SECRET_KEY'),
|
|
},
|
|
});
|
|
}
|
|
|
|
async upload(key: string, file: Buffer, options?: UploadOptions): Promise<UploadResult> {
|
|
const command = new PutObjectCommand({
|
|
Bucket: this.bucket,
|
|
Key: key,
|
|
Body: file,
|
|
ContentType: options?.contentType,
|
|
});
|
|
|
|
await this.client.send(command);
|
|
|
|
return {
|
|
key,
|
|
url: `https://${this.bucket}.s3.amazonaws.com/${key}`,
|
|
};
|
|
}
|
|
|
|
// ... otros metodos
|
|
}
|
|
```
|
|
|
|
### Uso
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class FileService {
|
|
constructor(private readonly storageFactory: StorageFactory) {}
|
|
|
|
async uploadProductImage(productId: string, file: Buffer): Promise<string> {
|
|
const storage = this.storageFactory.create(process.env.STORAGE_PROVIDER);
|
|
const key = `products/${productId}/image.jpg`;
|
|
|
|
const result = await storage.upload(key, file, {
|
|
contentType: 'image/jpeg',
|
|
});
|
|
|
|
return result.url;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Configuracion por Ambiente
|
|
|
|
| Ambiente | Proveedor | Razon |
|
|
|----------|-----------|-------|
|
|
| Development | MinIO | Local, sin credenciales |
|
|
| Staging | R2 | Costos bajos |
|
|
| Production | S3 o R2 | Segun necesidad |
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
- [AWS S3 SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/)
|
|
- [Cloudflare R2](https://developers.cloudflare.com/r2/)
|
|
- [MinIO](https://min.io/)
|
|
- [INT-011: Storage Cloud](../02-integraciones/INT-011-storage-cloud.md)
|
|
|
|
---
|
|
|
|
**Fecha decision:** 2026-01-10
|
|
**Autores:** Architecture Team
|