2026-03-06 17:28:17 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from supabase import create_client
|
|
|
|
|
|
|
|
|
|
from core.config.settings import SupabaseSettings, config
|
2026-03-13 14:10:13 +08:00
|
|
|
from core.config.settings import config as app_config
|
2026-03-06 17:28:17 +08:00
|
|
|
|
|
|
|
|
from .service_interface import BaseServiceProvider, register_service_instance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SupabaseService(BaseServiceProvider):
|
|
|
|
|
def __init__(self, settings: SupabaseSettings | None = None) -> None:
|
|
|
|
|
super().__init__("supabase")
|
|
|
|
|
self._settings = settings or config.supabase
|
|
|
|
|
self._client: Any = None
|
|
|
|
|
self._admin_client: Any = None
|
|
|
|
|
|
|
|
|
|
async def initialize(self, **_: Any) -> bool:
|
|
|
|
|
try:
|
2026-03-12 09:29:57 +08:00
|
|
|
self._init_clients()
|
2026-03-13 14:10:13 +08:00
|
|
|
await self._ensure_storage_bucket()
|
2026-03-06 17:28:17 +08:00
|
|
|
self._set_initialized(True)
|
|
|
|
|
self.logger.info("Supabase service initialized")
|
|
|
|
|
return True
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
2026-03-12 09:29:57 +08:00
|
|
|
self.logger.warning(
|
|
|
|
|
"Supabase service initialization failed", error=str(exc)
|
|
|
|
|
)
|
2026-03-06 17:28:17 +08:00
|
|
|
self._client = None
|
|
|
|
|
self._admin_client = None
|
|
|
|
|
self._set_initialized(False)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def close(self) -> bool:
|
|
|
|
|
self._client = None
|
|
|
|
|
self._admin_client = None
|
|
|
|
|
self._set_initialized(False)
|
|
|
|
|
self.logger.info("Supabase service closed")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
async def health_check(self) -> dict[str, Any]:
|
|
|
|
|
client = self._client
|
|
|
|
|
admin_client = self._admin_client
|
|
|
|
|
if client is None or admin_client is None:
|
|
|
|
|
return {"status": "unhealthy", "details": {"error": "not initialized"}}
|
|
|
|
|
try:
|
|
|
|
|
await asyncio.to_thread(client.auth.get_session)
|
2026-03-12 09:29:57 +08:00
|
|
|
await asyncio.to_thread(
|
|
|
|
|
admin_client.auth.admin.list_users, page=1, per_page=1
|
|
|
|
|
)
|
2026-03-06 17:28:17 +08:00
|
|
|
return {
|
|
|
|
|
"status": "healthy",
|
|
|
|
|
"details": {
|
|
|
|
|
"anon_client": "ready",
|
|
|
|
|
"admin_client": "ready",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
self.logger.warning("Supabase health check failed", error=str(exc))
|
|
|
|
|
return {"status": "unhealthy", "details": {"error": str(exc)}}
|
|
|
|
|
|
|
|
|
|
def get_client(self) -> Any:
|
|
|
|
|
return self._require_client()
|
|
|
|
|
|
|
|
|
|
def get_admin_client(self) -> Any:
|
|
|
|
|
return self._require_admin_client()
|
|
|
|
|
|
|
|
|
|
def _require_client(self) -> Any:
|
2026-03-12 09:29:57 +08:00
|
|
|
if self._client is None or self._admin_client is None:
|
|
|
|
|
self._init_clients()
|
|
|
|
|
self._set_initialized(True)
|
|
|
|
|
self.logger.info("Supabase service lazily initialized")
|
2026-03-06 17:28:17 +08:00
|
|
|
client = self._client
|
|
|
|
|
if client is None:
|
|
|
|
|
raise RuntimeError("Supabase client is not initialized")
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
|
def _require_admin_client(self) -> Any:
|
2026-03-12 09:29:57 +08:00
|
|
|
if self._client is None or self._admin_client is None:
|
|
|
|
|
self._init_clients()
|
|
|
|
|
self._set_initialized(True)
|
|
|
|
|
self.logger.info("Supabase service lazily initialized")
|
2026-03-06 17:28:17 +08:00
|
|
|
admin_client = self._admin_client
|
|
|
|
|
if admin_client is None:
|
|
|
|
|
raise RuntimeError("Supabase admin client is not initialized")
|
|
|
|
|
return admin_client
|
|
|
|
|
|
2026-03-12 09:29:57 +08:00
|
|
|
def _init_clients(self) -> None:
|
|
|
|
|
self._client = create_client(
|
|
|
|
|
self._settings.url,
|
|
|
|
|
self._settings.anon_key,
|
|
|
|
|
)
|
|
|
|
|
self._admin_client = create_client(
|
|
|
|
|
self._settings.url,
|
|
|
|
|
self._settings.service_role_key,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-13 14:10:13 +08:00
|
|
|
async def _ensure_storage_bucket(self) -> None:
|
|
|
|
|
bucket_name = app_config.storage.bucket
|
|
|
|
|
storage = getattr(self._admin_client, "storage", None)
|
|
|
|
|
if storage is None:
|
|
|
|
|
self.logger.warning("Storage client unavailable, skipping bucket check")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
get_bucket = getattr(storage, "get_bucket", None)
|
|
|
|
|
if not callable(get_bucket):
|
|
|
|
|
self.logger.warning("Storage get_bucket unavailable, skipping bucket check")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def _check_and_create() -> None:
|
|
|
|
|
try:
|
|
|
|
|
get_bucket(bucket_name)
|
|
|
|
|
self.logger.debug("Storage bucket already exists", bucket=bucket_name)
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
create_bucket = getattr(storage, "create_bucket", None)
|
|
|
|
|
if not callable(create_bucket):
|
|
|
|
|
self.logger.warning(
|
|
|
|
|
"Storage create_bucket unavailable, skipping bucket creation"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
create_bucket(bucket_name, options={"public": False})
|
|
|
|
|
self.logger.info("Storage bucket created", bucket=bucket_name)
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
msg = str(exc).lower()
|
|
|
|
|
if "already exists" in msg or "duplicate" in msg:
|
|
|
|
|
self.logger.debug(
|
|
|
|
|
"Storage bucket already exists (race)", bucket=bucket_name
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
self.logger.warning(
|
|
|
|
|
"Failed to create storage bucket",
|
|
|
|
|
bucket=bucket_name,
|
|
|
|
|
error=str(exc),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await asyncio.to_thread(_check_and_create)
|
|
|
|
|
|
2026-03-06 17:28:17 +08:00
|
|
|
|
|
|
|
|
supabase_service: SupabaseService = register_service_instance(
|
|
|
|
|
"supabase", SupabaseService()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
__all__ = ["SupabaseService", "supabase_service"]
|