## 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>
112 lines
4.3 KiB
SQL
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;
|