a65ad428e6
The runtime config supports relative paths like /api so the SPA can run same-origin in dev (Vite proxy) and stage/prod (Traefik). createDirectus from @directus/sdk calls new URL(...) internally, which throws on relative URLs. Resolve in toAbsoluteUrl() before handing to the SDK: '/api' -> 'http://localhost:5173/api' (dev) 'https://api.example.com' -> passes through unchanged Caught at runtime; typecheck/build don't surface this since the SDK treats its url arg as a string.
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
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<typeof buildClient>;
|
|
|
|
/**
|
|
* 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<Schema>(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
|
|
* `<RuntimeConfigProvider>` 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 <AuthBootstrap> inside <RuntimeConfigProvider>.',
|
|
);
|
|
}
|
|
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);
|
|
}
|