// Typed client for the anonymous menu API. It wraps an injected fetcher, so // the Nuxt composable decides the base URL and headers and `node --test` can // drive it with a stub. import type { Menu, MenuEnvelope } from './types.ts' /** Issues one GET for `path` (e.g. "/menus/main") with `query`, resolving the parsed JSON. */ export type MenuFetcher = (path: string, query?: Record) => Promise /** True for a fetch error that carries HTTP status 404 (ofetch, h3 or plain). */ export function isNotFound(err: unknown): boolean { if (!err || typeof err !== 'object') return false const e = err as { status?: unknown; statusCode?: unknown; response?: { status?: unknown } } return e.status === 404 || e.statusCode === 404 || e.response?.status === 404 } /** The API path of a menu: /menus/, the code URL-encoded. */ export function menuPath(code: string): string { return `/menus/${encodeURIComponent(code)}` } export interface MenuClient { /** The menu with `code`, or null when the API answers 404 (unknown or inactive). */ getMenu(code: string, locale?: string): Promise } export function createMenuClient(fetcher: MenuFetcher): MenuClient { return { async getMenu(code, locale) { const query = locale ? { locale } : undefined let res: MenuEnvelope try { res = (await (query ? fetcher(menuPath(code), query) : fetcher(menuPath(code)))) as MenuEnvelope } catch (err) { if (isNotFound(err)) return null throw err } const menu = res?.data if (!menu || typeof menu !== 'object') return null return { ...menu, items: Array.isArray(menu.items) ? menu.items : [] } }, } }