// lib/api/menu-api.ts import { fetchGraphQL } from './graphql-client.js'; import { cache } from '@lib/cache/manager.js'; import { CACHE_TTL } from '@lib/cache/cache-ttl'; export interface MenuItem { id: string; databaseId: number; uri: string; url: string; order: number; label: string; parentId: string | null; target: string; cssClasses: string[]; description: string; menuItemColor?: string | null; childItems?: { nodes: MenuItem[]; }; } export interface Menu { id: string; databaseId: number; name: string; slug: string; locations: string[]; menuItems: { nodes: MenuItem[]; }; } export type MenuIdentifier = | { id: number } // По ID меню | { location: string } // По локации | { slug: string } // По слагу | { name: string }; // По имени /** * Получить меню по идентификатору */ export async function fetchMenu(identifier: MenuIdentifier): Promise { // Создаем ключ кеша на основе типа и значения let cacheKey; if ('id' in identifier) cacheKey = `menu:id:${identifier.id}`; else if ('location' in identifier) cacheKey = `menu:location:${identifier.location}`; else if ('slug' in identifier) cacheKey = `menu:slug:${identifier.slug}`; else if ('name' in identifier) cacheKey = `menu:name:${identifier.name}`; else return null; return await cache.wrap( cacheKey, async () => { try { // Определяем тип запроса на основе переданного идентификатора if ('id' in identifier) { return await fetchMenuById(identifier.id); } if ('location' in identifier) { return await fetchMenuByLocation(identifier.location); } if ('slug' in identifier) { return await fetchMenuBySlug(identifier.slug); } if ('name' in identifier) { return await fetchMenuByName(identifier.name); } return null; } catch (error) { console.error('Error fetching menu:', error); return null; } }, { ttl: CACHE_TTL.MENU } // Используем константу из cache-ttl ); } /** * Получить меню по ID (самый надежный способ) */ async function fetchMenuById(id: number): Promise { const query = ` query GetMenuById($id: ID!) { menu(id: $id, idType: DATABASE_ID) { id databaseId name slug locations menuItems(first: 100) { nodes { id databaseId uri url order label parentId target cssClasses description menuItemColor childItems(first: 50) { nodes { id databaseId label uri url order menuItemColor } } } } } } `; const variables = { id }; const data = await fetchGraphQL(query, variables); if (data?.menu) { return { ...data.menu, menuItems: data.menu.menuItems || { nodes: [] } }; } return null; } /** * Получить меню по локации */ async function fetchMenuByLocation(location: string): Promise { const query = ` query GetMenuByLocation($location: MenuLocationEnum!) { menus(where: { location: $location }, first: 1) { nodes { id databaseId name slug locations menuItems(first: 100) { nodes { id databaseId uri url order label parentId target cssClasses description menuItemColor childItems(first: 50) { nodes { id databaseId label uri url order menuItemColor } } } } } } } `; const variables = { location }; const data = await fetchGraphQL(query, variables); return data?.menus?.nodes?.[0] || null; } /** * Получить меню по слагу */ async function fetchMenuBySlug(slug: string): Promise { const query = ` query GetMenuBySlug($slug: String!) { menus(where: { slug: $slug }, first: 1) { nodes { id databaseId name slug locations menuItems(first: 100) { nodes { id databaseId uri url order label parentId target cssClasses description menuItemColor } } } } } `; const variables = { slug }; const data = await fetchGraphQL(query, variables); return data?.menus?.nodes?.[0] || null; } /** * Получить меню по имени */ async function fetchMenuByName(name: string): Promise { const query = ` query GetMenuByName($name: String!) { menus(where: { name: $name }, first: 1) { nodes { id databaseId name slug locations menuItems(first: 100) { nodes { id databaseId uri url order label parentId target cssClasses description menuItemColor } } } } } `; const variables = { name }; const data = await fetchGraphQL(query, variables); return data?.menus?.nodes?.[0] || null; } /** * Преобразовать плоский список элементов меню в иерархическую структуру */ export function buildMenuHierarchy(menuItems: MenuItem[]): MenuItem[] { const itemsMap = new Map(); const rootItems: MenuItem[] = []; // Создаем map всех элементов menuItems.forEach(item => { itemsMap.set(item.id, { ...item, childItems: { nodes: [] } }); }); // Строим иерархию menuItems.forEach(item => { const menuItem = itemsMap.get(item.id)!; if (item.parentId && itemsMap.has(item.parentId)) { const parent = itemsMap.get(item.parentId)!; if (!parent.childItems) { parent.childItems = { nodes: [] }; } parent.childItems.nodes.push(menuItem); } else { rootItems.push(menuItem); } }); // Сортируем элементы по order const sortByOrder = (items: MenuItem[]) => items.sort((a, b) => a.order - b.order); // Рекурсивно сортируем все уровни function sortRecursive(items: MenuItem[]) { sortByOrder(items); items.forEach(item => { if (item.childItems?.nodes.length) { sortRecursive(item.childItems.nodes); } }); } sortRecursive(rootItems); return rootItems; } /** * Получить меню в виде иерархической структуры */ export async function getHierarchicalMenu(identifier: MenuIdentifier): Promise { const cacheKey = `menu:hierarchical:${JSON.stringify(identifier)}`; return await cache.wrap( cacheKey, async () => { const menu = await fetchMenu(identifier); if (!menu || !menu.menuItems?.nodes?.length) { return []; } return buildMenuHierarchy(menu.menuItems.nodes); }, { ttl: CACHE_TTL.MENU } ); } /** * Получить меню в виде плоского списка */ export async function getFlatMenu(identifier: MenuIdentifier): Promise { const cacheKey = `menu:flat:${JSON.stringify(identifier)}`; return await cache.wrap( cacheKey, async () => { const menu = await fetchMenu(identifier); if (!menu || !menu.menuItems?.nodes?.length) { return []; } return menu.menuItems.nodes.sort((a, b) => a.order - b.order); }, { ttl: CACHE_TTL.MENU } ); }