138 lines
5.6 KiB
Python
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
|