template-saas-database-v2/ddl/schemas/storage/tables/02-storage-usage.sql
rckrdmrd 3ce06fbce4 Initial commit - Database de template-saas migrado desde monorepo
Migración desde workspace-v2/projects/template-saas/apps/database
Este repositorio es parte del estándar multi-repo v2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:07:11 -06:00

179 lines
5.5 KiB
PL/PgSQL

-- ============================================
-- Storage Usage Table
-- Tracks storage usage per tenant
-- ============================================
CREATE TABLE storage.usage (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Usage metrics
total_files INTEGER NOT NULL DEFAULT 0 CHECK (total_files >= 0),
total_bytes BIGINT NOT NULL DEFAULT 0 CHECK (total_bytes >= 0),
-- By category
files_by_type JSONB DEFAULT '{}',
bytes_by_type JSONB DEFAULT '{}',
-- Limits from plan
max_bytes BIGINT,
max_file_size BIGINT,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- One record per tenant
CONSTRAINT uq_storage_usage_tenant UNIQUE (tenant_id)
);
-- Indexes
CREATE INDEX idx_storage_usage_tenant ON storage.usage(tenant_id);
-- RLS
ALTER TABLE storage.usage ENABLE ROW LEVEL SECURITY;
CREATE POLICY storage_usage_tenant_isolation ON storage.usage
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Comments
COMMENT ON TABLE storage.usage IS 'Aggregated storage usage per tenant';
COMMENT ON COLUMN storage.usage.files_by_type IS 'File count by mime type category: {images: 10, documents: 5}';
COMMENT ON COLUMN storage.usage.bytes_by_type IS 'Bytes by mime type category: {images: 1024000, documents: 512000}';
-- ============================================
-- Function: Update storage usage on file changes
-- ============================================
CREATE OR REPLACE FUNCTION storage.update_usage_on_file_change()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' AND NEW.status = 'ready' THEN
-- New file added
INSERT INTO storage.usage (tenant_id, total_files, total_bytes)
VALUES (NEW.tenant_id, 1, NEW.size_bytes)
ON CONFLICT (tenant_id)
DO UPDATE SET
total_files = storage.usage.total_files + 1,
total_bytes = storage.usage.total_bytes + NEW.size_bytes,
updated_at = NOW();
ELSIF TG_OP = 'UPDATE' AND OLD.status = 'ready' AND NEW.status != 'ready' THEN
-- File deleted/moved
UPDATE storage.usage
SET
total_files = GREATEST(0, total_files - 1),
total_bytes = GREATEST(0, total_bytes - OLD.size_bytes),
updated_at = NOW()
WHERE tenant_id = OLD.tenant_id;
ELSIF TG_OP = 'DELETE' AND OLD.status = 'ready' THEN
-- Hard delete
UPDATE storage.usage
SET
total_files = GREATEST(0, total_files - 1),
total_bytes = GREATEST(0, total_bytes - OLD.size_bytes),
updated_at = NOW()
WHERE tenant_id = OLD.tenant_id;
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_storage_usage
AFTER INSERT OR UPDATE OR DELETE ON storage.files
FOR EACH ROW
EXECUTE FUNCTION storage.update_usage_on_file_change();
COMMENT ON FUNCTION storage.update_usage_on_file_change IS 'Automatically update tenant storage usage';
-- ============================================
-- Function: Check if tenant can upload
-- ============================================
CREATE OR REPLACE FUNCTION storage.can_upload(
p_tenant_id UUID,
p_size_bytes BIGINT
) RETURNS BOOLEAN AS $$
DECLARE
v_current_bytes BIGINT;
v_max_bytes BIGINT;
v_max_file_size BIGINT;
BEGIN
SELECT total_bytes, max_bytes, max_file_size
INTO v_current_bytes, v_max_bytes, v_max_file_size
FROM storage.usage
WHERE tenant_id = p_tenant_id;
-- No usage record means no limit check needed yet
IF NOT FOUND THEN
RETURN TRUE;
END IF;
-- Check max file size
IF v_max_file_size IS NOT NULL AND p_size_bytes > v_max_file_size THEN
RETURN FALSE;
END IF;
-- Check total storage limit
IF v_max_bytes IS NOT NULL AND (v_current_bytes + p_size_bytes) > v_max_bytes THEN
RETURN FALSE;
END IF;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql STABLE;
COMMENT ON FUNCTION storage.can_upload IS 'Check if tenant can upload file of given size';
-- ============================================
-- Function: Get tenant storage stats
-- ============================================
CREATE OR REPLACE FUNCTION storage.get_tenant_stats(p_tenant_id UUID)
RETURNS TABLE (
total_files INTEGER,
total_bytes BIGINT,
max_bytes BIGINT,
max_file_size BIGINT,
usage_percent NUMERIC,
files_by_folder JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
COALESCE(u.total_files, 0)::INTEGER,
COALESCE(u.total_bytes, 0)::BIGINT,
u.max_bytes,
u.max_file_size,
CASE
WHEN u.max_bytes IS NULL OR u.max_bytes = 0 THEN 0
ELSE ROUND((u.total_bytes::NUMERIC / u.max_bytes * 100), 2)
END,
COALESCE(
(SELECT jsonb_object_agg(folder, count)
FROM (
SELECT folder, COUNT(*) as count
FROM storage.files
WHERE tenant_id = p_tenant_id AND deleted_at IS NULL
GROUP BY folder
) t),
'{}'::jsonb
)
FROM storage.usage u
WHERE u.tenant_id = p_tenant_id;
-- If no record, return defaults
IF NOT FOUND THEN
RETURN QUERY SELECT 0, 0::BIGINT, NULL::BIGINT, NULL::BIGINT, 0::NUMERIC, '{}'::JSONB;
END IF;
END;
$$ LANGUAGE plpgsql STABLE;
COMMENT ON FUNCTION storage.get_tenant_stats IS 'Get storage statistics for tenant';