347 lines
10 KiB
TypeScript
347 lines
10 KiB
TypeScript
// 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<Menu | null> {
|
|
// Создаем ключ кеша на основе типа и значения
|
|
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<Menu | null> {
|
|
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<Menu | null> {
|
|
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<Menu | null> {
|
|
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<Menu | null> {
|
|
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<string, MenuItem>();
|
|
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<MenuItem[]> {
|
|
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<MenuItem[]> {
|
|
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 }
|
|
);
|
|
} |