import { authentication, createDirectus, rest } from '@directus/sdk'; import { useRuntimeConfig } from '@/config/context'; /** * Minimum user fields we read after login. Mirrors what `/users/me` returns * with `?fields=id,email,role,first_name,last_name`. */ export type DirectusUser = { id: string; email: string | null; role: string | null; first_name: string | null; last_name: string | null; }; /** * Typed Directus collection schema for the SDK. * * Will grow as the SPA starts reading TRM-specific collections in Phase 2+. * Empty for now — Phase 1 only needs `/auth/*` and `/users/me`, which the * SDK exposes outside the schema. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export type Schema = {}; type DirectusClient = ReturnType; /** * Resolve a (possibly relative) URL against the current page origin. * * Runtime config supports relative paths like `/api` because the SPA always * runs same-origin to its backends. The Directus SDK, however, calls * `new URL(...)` internally and requires an absolute URL — so we resolve * before handing it over. * * - `'/api'` → `'http://localhost:5173/api'` (in dev) * - `'https://api.example.com'` → `'https://api.example.com/'` (passes through) */ function toAbsoluteUrl(maybeRelative: string): string { return new URL(maybeRelative, window.location.origin).toString(); } function buildClient(directusUrl: string) { return createDirectus(toAbsoluteUrl(directusUrl)) .with(authentication('cookie', { credentials: 'include', autoRefresh: true })) .with(rest({ credentials: 'include' })); } let _client: DirectusClient | null = null; let _clientUrl: string | null = null; /** * Lazy singleton accessor for the Directus client. * * The runtime config must be loaded before this is called (the * `` enforces that by gating its children). * If the config URL changes between calls (e.g. dev HMR), the client is * rebuilt to match — a minor convenience, not a production concern. */ export function getDirectus(): DirectusClient { // We can't call useRuntimeConfig outside React; the consumers of getDirectus // are non-React (Zustand actions). They pass the URL via a small adapter // below or read it from a non-React-bound source. if (!_client) { throw new Error( 'getDirectus() called before initDirectusClient(). Wire inside .', ); } return _client; } /** * Initialise the Directus client with the runtime config's `directusUrl`. * Idempotent: rebuilds only if the URL has changed (HMR-friendly). */ export function initDirectusClient(directusUrl: string): DirectusClient { if (_client && _clientUrl === directusUrl) return _client; _client = buildClient(directusUrl); _clientUrl = directusUrl; return _client; } /** * Convenience hook for components that need a typed Directus client. * Inside React, prefer this over `getDirectus()` — it's reactive to runtime * config changes (in practice config is immutable, but the hook form is * cleaner). */ export function useDirectus(): DirectusClient { const cfg = useRuntimeConfig(); return initDirectusClient(cfg.directusUrl); }