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>
226 lines
6.0 KiB
PL/PgSQL
226 lines
6.0 KiB
PL/PgSQL
-- ============================================
|
|
-- TEMPLATE-SAAS: Core Functions
|
|
-- Version: 1.0.0
|
|
-- ============================================
|
|
|
|
-- ============================================
|
|
-- TENANT CONTEXT FUNCTIONS
|
|
-- ============================================
|
|
|
|
-- Set current tenant for RLS
|
|
CREATE OR REPLACE FUNCTION auth.set_current_tenant(p_tenant_id UUID)
|
|
RETURNS VOID AS $$
|
|
BEGIN
|
|
PERFORM set_config('app.current_tenant_id', p_tenant_id::TEXT, FALSE);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Get current tenant
|
|
CREATE OR REPLACE FUNCTION auth.get_current_tenant()
|
|
RETURNS UUID AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_tenant_id', TRUE)::UUID;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Set current user
|
|
CREATE OR REPLACE FUNCTION auth.set_current_user(p_user_id UUID)
|
|
RETURNS VOID AS $$
|
|
BEGIN
|
|
PERFORM set_config('app.current_user_id', p_user_id::TEXT, FALSE);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Get current user
|
|
CREATE OR REPLACE FUNCTION auth.get_current_user()
|
|
RETURNS UUID AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_user_id', TRUE)::UUID;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Clear context
|
|
CREATE OR REPLACE FUNCTION auth.clear_context()
|
|
RETURNS VOID AS $$
|
|
BEGIN
|
|
PERFORM set_config('app.current_tenant_id', '', FALSE);
|
|
PERFORM set_config('app.current_user_id', '', FALSE);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- ============================================
|
|
-- PLAN & LIMITS FUNCTIONS
|
|
-- ============================================
|
|
|
|
-- Get tenant's current plan limits
|
|
CREATE OR REPLACE FUNCTION plans.get_tenant_limits(p_tenant_id UUID)
|
|
RETURNS JSONB AS $$
|
|
DECLARE
|
|
v_limits JSONB;
|
|
BEGIN
|
|
SELECT p.limits INTO v_limits
|
|
FROM tenants.tenants t
|
|
JOIN plans.plans p ON t.plan_id = p.id
|
|
WHERE t.id = p_tenant_id;
|
|
|
|
RETURN COALESCE(v_limits, '{}'::jsonb);
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Check if tenant can perform action based on limits
|
|
CREATE OR REPLACE FUNCTION plans.check_limit(
|
|
p_tenant_id UUID,
|
|
p_limit_key VARCHAR,
|
|
p_current_count INT
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_limit INT;
|
|
BEGIN
|
|
SELECT (plans.get_tenant_limits(p_tenant_id)->>p_limit_key)::INT INTO v_limit;
|
|
|
|
IF v_limit IS NULL THEN
|
|
RETURN TRUE; -- No limit defined
|
|
END IF;
|
|
|
|
RETURN p_current_count < v_limit;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Get tenant's feature flags
|
|
CREATE OR REPLACE FUNCTION plans.get_tenant_features(p_tenant_id UUID)
|
|
RETURNS JSONB AS $$
|
|
DECLARE
|
|
v_features JSONB;
|
|
BEGIN
|
|
SELECT p.included_features INTO v_features
|
|
FROM tenants.tenants t
|
|
JOIN plans.plans p ON t.plan_id = p.id
|
|
WHERE t.id = p_tenant_id;
|
|
|
|
RETURN COALESCE(v_features, '[]'::jsonb);
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Check if tenant has feature
|
|
CREATE OR REPLACE FUNCTION plans.has_feature(
|
|
p_tenant_id UUID,
|
|
p_feature_code VARCHAR
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
RETURN plans.get_tenant_features(p_tenant_id) ? p_feature_code;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- ============================================
|
|
-- USER COUNT FUNCTIONS
|
|
-- ============================================
|
|
|
|
-- Count active users in tenant
|
|
CREATE OR REPLACE FUNCTION users.count_active_users(p_tenant_id UUID)
|
|
RETURNS INT AS $$
|
|
BEGIN
|
|
RETURN (
|
|
SELECT COUNT(*)
|
|
FROM users.users
|
|
WHERE tenant_id = p_tenant_id
|
|
AND status = 'active'
|
|
AND deleted_at IS NULL
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- Check if tenant can add more users
|
|
CREATE OR REPLACE FUNCTION users.can_add_user(p_tenant_id UUID)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
RETURN plans.check_limit(
|
|
p_tenant_id,
|
|
'max_users',
|
|
users.count_active_users(p_tenant_id)
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
-- ============================================
|
|
-- UTILITY FUNCTIONS
|
|
-- ============================================
|
|
|
|
-- Update updated_at column automatically
|
|
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Create AI configs updated_at trigger (table defined in schemas/ai/)
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'configs') THEN
|
|
DROP TRIGGER IF EXISTS update_ai_configs_updated_at ON ai.configs;
|
|
CREATE TRIGGER update_ai_configs_updated_at
|
|
BEFORE UPDATE ON ai.configs
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Create webhooks updated_at trigger (table defined in schemas/webhooks/)
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'webhooks' AND table_name = 'webhooks') THEN
|
|
DROP TRIGGER IF EXISTS webhooks_updated_at ON webhooks.webhooks;
|
|
CREATE TRIGGER webhooks_updated_at
|
|
BEFORE UPDATE ON webhooks.webhooks
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Generate slug from name
|
|
CREATE OR REPLACE FUNCTION public.slugify(p_text VARCHAR)
|
|
RETURNS VARCHAR AS $$
|
|
BEGIN
|
|
RETURN LOWER(
|
|
REGEXP_REPLACE(
|
|
REGEXP_REPLACE(
|
|
TRIM(p_text),
|
|
'[^a-zA-Z0-9\s-]', '', 'g'
|
|
),
|
|
'\s+', '-', 'g'
|
|
)
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
-- Generate random token
|
|
CREATE OR REPLACE FUNCTION public.generate_token(p_length INT DEFAULT 32)
|
|
RETURNS VARCHAR AS $$
|
|
BEGIN
|
|
RETURN encode(gen_random_bytes(p_length), 'hex');
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Hash token
|
|
CREATE OR REPLACE FUNCTION public.hash_token(p_token VARCHAR)
|
|
RETURNS VARCHAR AS $$
|
|
BEGIN
|
|
RETURN encode(digest(p_token, 'sha256'), 'hex');
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
-- Comments
|
|
COMMENT ON FUNCTION auth.set_current_tenant IS 'Set tenant context for RLS policies';
|
|
COMMENT ON FUNCTION auth.get_current_tenant IS 'Get current tenant from context';
|
|
COMMENT ON FUNCTION plans.check_limit IS 'Check if action is within plan limits';
|
|
COMMENT ON FUNCTION plans.has_feature IS 'Check if tenant has access to feature';
|