first commit
This commit is contained in:
168
backend/app/repositories/directus/client.py
Normal file
168
backend/app/repositories/directus/client.py
Normal file
@@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class DirectusClient:
|
||||
"""Small wrapper around Directus HTTP APIs used by FastAPI repositories.
|
||||
|
||||
We keep this adapter thin on purpose:
|
||||
- business rules stay in services/use-cases
|
||||
- repository classes decide which collection to access
|
||||
- this client only knows how to talk to Directus
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str | None = None,
|
||||
token: str | None = None,
|
||||
timeout: float = 15.0,
|
||||
) -> None:
|
||||
self.base_url = (base_url or settings.directus_base_url).rstrip("/")
|
||||
self.token = token or settings.directus_admin_token
|
||||
self.timeout = timeout or settings.directus_timeout
|
||||
|
||||
def _headers(self) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if self.token:
|
||||
headers["Authorization"] = f"Bearer {self.token}"
|
||||
return headers
|
||||
|
||||
def _headers_for_token(self, token: str | None = None) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
elif self.token:
|
||||
headers["Authorization"] = f"Bearer {self.token}"
|
||||
return headers
|
||||
|
||||
async def list_items(
|
||||
self,
|
||||
collection: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
access_token: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Fetch a collection list from Directus."""
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/items/{collection}",
|
||||
headers=self._headers_for_token(access_token),
|
||||
params=params,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return payload.get("data", [])
|
||||
|
||||
async def get_item(
|
||||
self,
|
||||
collection: str,
|
||||
item_id: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
access_token: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Fetch a single item from Directus."""
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/items/{collection}/{item_id}",
|
||||
headers=self._headers_for_token(access_token),
|
||||
params=params,
|
||||
)
|
||||
if response.status_code == 404:
|
||||
return None
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return payload.get("data")
|
||||
|
||||
async def create_item(
|
||||
self,
|
||||
collection: str,
|
||||
data: dict[str, Any],
|
||||
access_token: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a Directus item so activity/revision can still be preserved."""
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/items/{collection}",
|
||||
headers=self._headers_for_token(access_token),
|
||||
json=data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return payload["data"]
|
||||
|
||||
async def update_item(
|
||||
self,
|
||||
collection: str,
|
||||
item_id: str,
|
||||
data: dict[str, Any],
|
||||
access_token: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Update a Directus item after FastAPI finishes validation and orchestration."""
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.patch(
|
||||
f"{self.base_url}/items/{collection}/{item_id}",
|
||||
headers=self._headers_for_token(access_token),
|
||||
json=data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return payload["data"]
|
||||
|
||||
async def delete_item(
|
||||
self,
|
||||
collection: str,
|
||||
item_id: str,
|
||||
access_token: str | None = None,
|
||||
) -> None:
|
||||
"""Delete a Directus item."""
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.delete(
|
||||
f"{self.base_url}/items/{collection}/{item_id}",
|
||||
headers=self._headers_for_token(access_token),
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
async def list_activity(
|
||||
self,
|
||||
params: dict[str, Any] | None = None,
|
||||
access_token: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Query Directus /activity — the built-in audit trail for all collections."""
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/activity",
|
||||
headers=self._headers_for_token(access_token),
|
||||
params=params,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return payload.get("data", [])
|
||||
|
||||
async def get_current_user(
|
||||
self,
|
||||
access_token: str,
|
||||
fields: list[str] | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Resolve Directus /users/me with a bearer token issued by Directus."""
|
||||
url = f"{self.base_url}/users/me"
|
||||
if fields:
|
||||
# Directus accepts comma-delimited `fields=...` query strings, but in
|
||||
# this environment it falls back to returning only `{id}` when the
|
||||
# commas are percent-encoded by the HTTP client. Build the query
|
||||
# string explicitly so auth/role bootstrap remains deterministic.
|
||||
url = f"{url}?fields={','.join(fields)}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
url,
|
||||
headers=self._headers_for_token(access_token),
|
||||
)
|
||||
if response.status_code == 401:
|
||||
return None
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return payload.get("data")
|
||||
Reference in New Issue
Block a user