michangarrito/database/schemas/17-storage.sql
rckrdmrd 5a49ad0185 [INTEGRATION] feat: Integrate template-saas scopes and database objects
## Documentation
- Align MCH-029 to MCH-032 with template-saas modules (SAAS-008 to SAAS-015)
- Create MCH-034 (Analytics) and MCH-035 (Reports) from SAAS-016/017
- Update PLAN-DESARROLLO.md with Phase 7 and 8
- Update _MAP.md indexes (35 total epics)

## Database (5 new schemas, 14 tables)
- Add storage schema: buckets, files, signed_urls
- Add webhooks schema: endpoints, deliveries
- Add audit schema: logs, retention_policies
- Add features schema: flags, tenant_flags (14 seeds)
- Add analytics schema: metrics, events, reports, report_schedules
- Add auth.oauth_connections for MCH-030
- Add timestamptz_to_date() IMMUTABLE function
- Update EXPECTED_SCHEMAS in recreate-database.sh

## Analysis Reports
- ANALISIS-INTEGRACION-TEMPLATE-SAAS-2026-01-13.md
- VALIDACION-COHERENCIA-2026-01-13.md
- GAP-ANALYSIS-BD-2026-01-13.md
- REPORTE-EJECUCION-2026-01-13.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 07:10:55 -06:00

112 lines
4.3 KiB
SQL

-- =============================================================================
-- MICHANGARRITO - 17 STORAGE (MCH-029: Infraestructura SaaS)
-- =============================================================================
-- Sistema de almacenamiento de archivos multi-tenant
-- Integra con: S3, Cloudflare R2, o almacenamiento local
-- =============================================================================
-- Buckets de almacenamiento
CREATE TABLE IF NOT EXISTS storage.buckets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
-- Configuracion
public BOOLEAN DEFAULT false,
file_size_limit INTEGER, -- bytes, null = sin limite
allowed_mime_types TEXT[],
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TRIGGER update_storage_buckets_updated_at
BEFORE UPDATE ON storage.buckets
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE storage.buckets IS 'Buckets de almacenamiento (contenedores)';
COMMENT ON COLUMN storage.buckets.file_size_limit IS 'Limite de tamaño por archivo en bytes';
COMMENT ON COLUMN storage.buckets.allowed_mime_types IS 'Array de MIME types permitidos';
-- Archivos almacenados
CREATE TABLE IF NOT EXISTS storage.files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
bucket_id UUID NOT NULL REFERENCES storage.buckets(id) ON DELETE CASCADE,
-- Identificacion
name VARCHAR(255) NOT NULL,
path VARCHAR(500) NOT NULL,
-- Propiedades del archivo
mime_type VARCHAR(100),
size INTEGER NOT NULL,
checksum VARCHAR(64), -- SHA-256
-- Metadata adicional
metadata JSONB DEFAULT '{}',
-- Control de acceso
is_public BOOLEAN DEFAULT false,
-- Referencias
uploaded_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Constraints
UNIQUE(bucket_id, path)
);
CREATE INDEX idx_storage_files_tenant ON storage.files(tenant_id);
CREATE INDEX idx_storage_files_bucket ON storage.files(bucket_id);
CREATE INDEX idx_storage_files_path ON storage.files(path);
CREATE INDEX idx_storage_files_mime ON storage.files(mime_type);
CREATE TRIGGER update_storage_files_updated_at
BEFORE UPDATE ON storage.files
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE storage.files IS 'Archivos almacenados por tenant';
COMMENT ON COLUMN storage.files.path IS 'Ruta completa del archivo: tenant_id/bucket/filename';
COMMENT ON COLUMN storage.files.metadata IS 'Metadata adicional: {width, height, duration, etc.}';
-- URLs firmadas (para acceso temporal)
CREATE TABLE IF NOT EXISTS storage.signed_urls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_id UUID NOT NULL REFERENCES storage.files(id) ON DELETE CASCADE,
-- Token y expiracion
token VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
-- Restricciones
max_downloads INTEGER,
downloads_count INTEGER DEFAULT 0,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL
);
CREATE INDEX idx_storage_signed_urls_token ON storage.signed_urls(token);
CREATE INDEX idx_storage_signed_urls_expires ON storage.signed_urls(expires_at);
COMMENT ON TABLE storage.signed_urls IS 'URLs firmadas para acceso temporal a archivos privados';
-- =============================================================================
-- SEEDS: Buckets por defecto
-- =============================================================================
INSERT INTO storage.buckets (name, description, public, file_size_limit, allowed_mime_types) VALUES
('products', 'Imagenes de productos', true, 5242880, ARRAY['image/jpeg', 'image/png', 'image/webp']),
('avatars', 'Fotos de perfil de usuarios', true, 1048576, ARRAY['image/jpeg', 'image/png']),
('documents', 'Documentos privados', false, 10485760, ARRAY['application/pdf', 'image/jpeg', 'image/png']),
('exports', 'Archivos de exportacion (reportes)', false, 52428800, ARRAY['application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv']),
('backups', 'Backups de datos', false, NULL, NULL)
ON CONFLICT (name) DO NOTHING;