"""Generic read-only Graph passthrough. The typed endpoints cover the entities and metrics we expect to use daily, but the Graph API is far larger than that (pages, Instagram accounts, custom audiences, ad rules, …). Rather than force a deploy every time something new is needed, this exposes the whole **read** surface behind one endpoint. It is GET-only by construction, so it cannot be used to create, modify or delete anything - the read-only stance of this phase holds even here. Writes will be explicit, typed endpoints with their own guard rails (see documentation/overview.md). Credentials: see ``app.credentials.get_meta_credentials``. """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, Path, Query, Request from ..clients.graph import MetaGraphClient from ..credentials import MetaCredentials, get_meta_credentials from ..errors import UpstreamError router = APIRouter(prefix="/graph", tags=["meta-ads: generic read"]) # Params the proxy owns. A caller must not be able to override the credentials # we derived from the headers, and all_pages is ours, not Graph's. _RESERVED_PARAMS = {"access_token", "appsecret_proof", "all_pages"} @router.get( "/{graph_path:path}", summary="Any Graph API GET (read-only escape hatch)", ) async def graph_get( request: Request, graph_path: str = Path( ..., description="Graph path WITHOUT the version prefix, e.g. " "'act_123456/customaudiences' or '17841400000000000/media'.", ), fields: str | None = Query(None, description="Comma-separated Graph fields."), limit: int | None = Query(None, ge=1, le=500), after: str | None = Query(None, description="Paging cursor."), all_pages: bool = Query( True, description="Follow paging.next and return every page (default, capped " "by META_MAX_PAGES). Set false for a single raw page.", ), creds: MetaCredentials = Depends(get_meta_credentials), ) -> Any: path = graph_path.strip().lstrip("/") if not path: raise UpstreamError("A Graph path is required.", status=400) # The version comes from config/X-Meta-Api-Version; a version in the path # would silently override that, so reject it instead of double-prefixing. first = path.split("/", 1)[0] if first.startswith("v") and first[1:].replace(".", "").isdigit(): raise UpstreamError( f"Do not include the API version ('{first}') in the path - it is " "taken from X-Meta-Api-Version or the service default.", status=400, ) # Forward any extra query params verbatim so the full Graph surface stays # reachable, minus the ones the proxy controls. params: dict[str, Any] = { key: value for key, value in request.query_params.items() if key not in _RESERVED_PARAMS } params.update({"fields": fields, "limit": limit, "after": after}) client = MetaGraphClient(creds, service_name="Meta Graph API") if all_pages: return await client.get_all_pages(path, params) return await client.get(path, params)