Files
remnawave-minishop/backend/db/dal/app_settings_dal.py
T

112 lines
3.2 KiB
Python

"""Persistent overrides for application settings.
Overrides take priority over `.env` values for keys exposed via the admin
manifest. Values are stored as JSON-encoded text to preserve typing across
strings, booleans, integers and floats.
"""
import json
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import delete, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
from db.models import AppSettingOverride
logger = logging.getLogger(__name__)
def _encode(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def _decode(raw: Optional[str]) -> Any:
if raw is None:
return None
try:
return json.loads(raw)
except (TypeError, ValueError):
return raw
async def get_all_overrides(session: AsyncSession) -> Dict[str, Any]:
rows = (await session.execute(select(AppSettingOverride))).scalars().all()
return {row.key: _decode(row.value) for row in rows}
async def get_override_value(session: AsyncSession, key: str) -> Tuple[bool, Any]:
row = (
await session.execute(
select(AppSettingOverride).where(AppSettingOverride.key == key).limit(1)
)
).scalar_one_or_none()
if row is None:
return False, None
return True, _decode(row.value)
async def get_overrides_with_meta(session: AsyncSession) -> List[Dict[str, Any]]:
rows = (await session.execute(select(AppSettingOverride))).scalars().all()
items: List[Dict[str, Any]] = []
for row in rows:
items.append(
{
"key": row.key,
"value": _decode(row.value),
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
"updated_by": row.updated_by,
}
)
return items
async def upsert_override(
session: AsyncSession,
*,
key: str,
value: Any,
updated_by: Optional[int],
) -> None:
encoded = _encode(value)
now = datetime.now(timezone.utc)
stmt = (
pg_insert(AppSettingOverride)
.values(key=key, value=encoded, updated_at=now, updated_by=updated_by)
.on_conflict_do_update(
index_elements=[AppSettingOverride.key],
set_={
"value": encoded,
"updated_at": now,
"updated_by": updated_by,
},
)
)
await session.execute(stmt)
async def delete_override(session: AsyncSession, key: str) -> bool:
stmt = delete(AppSettingOverride).where(AppSettingOverride.key == key)
result = await session.execute(stmt)
return bool(result.rowcount or 0)
async def bulk_apply(
session: AsyncSession,
*,
updates: Dict[str, Tuple[bool, Any]],
updated_by: Optional[int],
) -> None:
"""Apply a batch of changes. Each entry maps key -> (set_flag, value).
When set_flag is False the override is deleted (revert to env). Otherwise
the value is upserted.
"""
for key, (set_flag, value) in updates.items():
if set_flag:
await upsert_override(session, key=key, value=value, updated_by=updated_by)
else:
await delete_override(session, key)