Template base para proyectos SaaS multi-tenant. Estructura inicial: - apps/backend (NestJS API) - apps/frontend (React/Vite) - apps/database (PostgreSQL DDL) - docs/ (Documentación) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
193 lines
4.9 KiB
PL/PgSQL
193 lines
4.9 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
|
|
-- ============================================
|
|
|
|
-- 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';
|