Files
plantillas-proyectos/backend/api/v1/modules/a76/app_settings/service.py

138 lines
5.6 KiB
Python

from typing import Optional, List, Dict, Any
from sqlalchemy import or_, and_, select, case, nulls_first
from sqlalchemy.orm import Session
from .models import AppSetting
import logging
logger = logging.getLogger(__name__)
from decimal import Decimal
def convert_decimals(obj: Any) -> Any:
"""
Recursively converts Decimal objects to floats for JSON serialization.
"""
if isinstance(obj, list):
return [convert_decimals(i) for i in obj]
elif isinstance(obj, dict):
return {k: convert_decimals(v) for k, v in obj.items()}
elif isinstance(obj, Decimal):
return float(obj)
return obj
def deep_merge(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively merges dict2 into dict1.
"""
for key, value in dict2.items():
if key in dict1 and isinstance(dict1[key], dict) and isinstance(value, dict):
deep_merge(dict1[key], value)
else:
dict1[key] = value
return dict1
class AppSettingsService:
"""
Service to manage hierarchical configuration overrides.
Hierarchy: System (Global) -> Tenant -> Company.
"""
@staticmethod
def get_resolved_settings(db: Session, tenant_id: int, company_id: int) -> Dict[str, Any]:
"""
Retrieves settings from all levels (Global -> Tenant -> Company) and merges them.
"""
stmt = (
select(AppSetting)
.where(
or_(
and_(AppSetting.tenant_id.is_(None), AppSetting.company_id.is_(None)),
and_(AppSetting.tenant_id == tenant_id, AppSetting.company_id.is_(None)),
and_(AppSetting.tenant_id == tenant_id, AppSetting.company_id == company_id),
)
)
.order_by(
# Ensure the order is: Global (1) -> Tenant (2) -> Company (3)
case(
(and_(AppSetting.tenant_id.is_(None), AppSetting.company_id.is_(None)), 1),
(and_(AppSetting.tenant_id.is_not(None), AppSetting.company_id.is_(None)), 2),
(and_(AppSetting.tenant_id.is_not(None), AppSetting.company_id.is_not(None)), 3),
else_=4
).asc()
)
)
results = db.execute(stmt).scalars().all()
logger.info(f"RESOLVE: Found {len(results)} rows for Hierarchy")
resolved_settings = {}
for row in results:
level_name = "GLOBAL" if not row.tenant_id else ("TENANT" if not row.company_id else "COMPANY")
# Use safe data logging to avoid crashes
settings_data = row.settings if row.settings else {}
keys = list(settings_data.keys()) if isinstance(settings_data, dict) else "not-a-dict"
logger.info(f"RESOLVE: Merging {level_name} layer with keys: {keys}")
if isinstance(settings_data, dict):
deep_merge(resolved_settings, settings_data)
return resolved_settings
@staticmethod
def upsert_settings(db: Session, tenant_id: Optional[int], company_id: Optional[int], settings: Dict[str, Any]) -> AppSetting:
"""
Inserts or updates settings for a specific level.
"""
level_label = f"level(tenant={tenant_id}, company={company_id})"
logger.info(f"UPSERT Settings START: {level_label}, keys_to_update={list(settings.keys())}")
# Ensure all Decimals are converted to floats before deep merge and save
settings = convert_decimals(settings)
stmt = select(AppSetting).where(
and_(
AppSetting.tenant_id == tenant_id if tenant_id is not None else AppSetting.tenant_id.is_(None),
AppSetting.company_id == company_id if company_id is not None else AppSetting.company_id.is_(None)
)
)
existing = db.execute(stmt).scalar_one_or_none()
if existing:
# Deep merge at the root level (merging categories like ssisgen, ssismex, etc.)
logger.info(f"UPSERT: Updating existing row ID={existing.id}")
# Create a shallow copy of the top-level dict to ensure SQLAlchemy sees a new reference
new_settings = dict(existing.settings) if existing.settings else {}
# Detailed logging of what's changing
for cat, data in settings.items():
old_keys = list(new_settings.get(cat, {}).keys())
new_keys = list(data.keys()) if isinstance(data, dict) else []
logger.info(f"UPSERT: Merging category [{cat}]. Old keys: {old_keys}, New keys to merge/overwrite: {new_keys}")
deep_merge(new_settings, settings)
# Second pass of conversion (merged results might still have Decimals if original row had them)
existing.settings = convert_decimals(new_settings)
from sqlalchemy.orm.attributes import flag_modified
flag_modified(existing, "settings")
else:
logger.info(f"UPSERT: Creating NEW row for {level_label}")
existing = AppSetting(
tenant_id=tenant_id,
company_id=company_id,
settings=settings
)
db.add(existing)
try:
db.commit()
db.refresh(existing)
logger.info(f"UPSERT SUCCESS: Row ID={existing.id}, Final Settings Hash Keys={list(existing.settings.keys())}")
except Exception as e:
db.rollback()
logger.error(f"UPSERT FAILED: {str(e)}")
raise e
return existing