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>
179 lines
5.5 KiB
PL/PgSQL
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';
|