UNPKG

iluria-sdk

Version:

SDK oficial do Iluria para integração com lojas e criação de frontends customizados

460 lines (397 loc) 12.6 kB
const GraphQLQueryBuilder = require('./graphql-builder'); const CacheManager = require('./cache'); const { NoOpCache } = require('./cache'); /** * Menu module - Gerencia menus via GraphQL */ class Menu { constructor(httpClient, cacheManager = null, config = {}) { this.http = httpClient; this.config = config; // Usa o cache manager fornecido ou cria um padrão if (cacheManager === false) { this.cache = new NoOpCache(); } else if (cacheManager) { this.cache = cacheManager; } else { // Mantém compatibilidade: cria cache simples se não fornecido this.cache = new CacheManager({ maxMemorySize: 50, ttl: 300000, // 5 minutes cache (menus mudam menos frequentemente) storage: false // Apenas memória por padrão }); } // TTL padrão para menus this.cacheTTL = 300000; // 5 minutes cache if (this.config.debug) { console.log('[Menu] Initialized with cache:', !!this.cache); } } /** * Lista todos os menus com filtros e campos customizados * @param {object} options - Opções de filtro (limit, offset, search, orderBy) * @param {Array<string>} fields - Campos a buscar para cada menu * @returns {object} - Lista de menus com paginação */ async list(options = {}, fields = null) { // Type safety validation if (options && typeof options !== 'object') { throw new Error('Menu.list: options must be an object'); } if (fields && !Array.isArray(fields)) { throw new Error('Menu.list: fields must be an array of strings'); } // Campos padrão se não especificados if (!fields) { fields = [ 'id', 'name', 'itemsCount', 'totalItemsCount', 'createdAt', 'updatedAt', 'menuItems.id', 'menuItems.name', 'menuItems.link', 'menuItems.position', 'menuItems.canHaveChildren', 'menuItems.childrenCount', 'menuItems.children.id', 'menuItems.children.name', 'menuItems.children.link', 'menuItems.children.position' ]; } if (this.config.debug) { console.log('[Menu] Listing menus with options:', options); console.log('[Menu] Requested fields:', fields); } // Gera chave de cache const cacheKey = `menus:${JSON.stringify(options)}:${fields.join(',')}`; // Verifica cache primeiro if (this.cache) { const cached = await this.cache.get(cacheKey); if (cached) { if (this.config.debug) { console.log('[Menu] Cache hit for menus list'); } return cached; } } // Constrói query GraphQL para buscar menus diretamente na raiz // Converte campos com notação de ponto para estrutura GraphQL aninhada const buildGraphQLFields = (fields) => { const fieldMap = {}; fields.forEach(field => { const parts = field.split('.'); let current = fieldMap; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (i === parts.length - 1) { // Campo final current[part] = true; } else { // Campo aninhado if (!current[part]) { current[part] = {}; } current = current[part]; } } }); const buildString = (obj) => { const keys = Object.keys(obj); const fields = []; keys.forEach(key => { if (obj[key] === true) { fields.push(key); } else { fields.push(`${key} { ${buildString(obj[key])} }`); } }); return fields.join(' '); }; return buildString(fieldMap); }; const menuFields = buildGraphQLFields(fields); const query = ` query GetMenus($limit: Int, $offset: Int, $search: String, $orderBy: MenuOrderBy) { menus(limit: $limit, offset: $offset, search: $search, orderBy: $orderBy) { items { ${menuFields} } totalCount hasMore offset limit } } `; const variables = { limit: options.limit || 10, offset: options.offset || 0, search: options.search || null, orderBy: options.orderBy || 'NAME_ASC' }; if (this.config.debug) { console.log('[Menu] GraphQL query built:', query); console.log('[Menu] Variables:', variables); } try { const data = await this.http.graphql(query, variables); if (this.config.debug) { console.log('[Menu] Menus fetched successfully:', data.menus?.items?.length || 0); } // Salva no cache if (this.cache && data.menus) { await this.cache.set(cacheKey, data.menus, this.cacheTTL); if (this.config.debug) { console.log('[Menu] Menus cached'); } } return data.menus; } catch (error) { console.error('[Menu] Error fetching menus:', error); throw error; } } /** * Busca um menu específico por ID * @param {string} id - ID do menu (formato ULID) * @param {Array<string>} fields - Campos a buscar * @returns {object} - Dados do menu */ async get(id, fields = null) { // Type safety validation if (!id || typeof id !== 'string') { throw new Error('Menu.get: id must be a non-empty string'); } // Validate ULID format if (!id.match(/^[0-9A-Z]{26}$/)) { throw new Error('Menu.get: id must be a valid ULID (26 alphanumeric characters)'); } if (fields && !Array.isArray(fields)) { throw new Error('Menu.get: fields must be an array of strings'); } // Campos padrão se não especificados if (!fields) { fields = [ 'id', 'name', 'itemsCount', 'totalItemsCount', 'createdAt', 'updatedAt', 'menuItems.id', 'menuItems.name', 'menuItems.link', 'menuItems.position', 'menuItems.canHaveChildren', 'menuItems.childrenCount', 'menuItems.children.id', 'menuItems.children.name', 'menuItems.children.link', 'menuItems.children.position' ]; } // Gera chave de cache const cacheKey = `menu:${id}:${fields.join(',')}`; if (this.config.debug) { console.log(`[Menu] Getting menu by ID: ${id}`); } // Verifica cache primeiro if (this.cache) { const cached = await this.cache.get(cacheKey); if (cached) { if (this.config.debug) { console.log(`[Menu] Cache hit for menu: ${nameOrId}`); } return cached; } } // Constrói query GraphQL para buscar menu individual // Converte campos com notação de ponto para estrutura GraphQL aninhada const buildGraphQLFields = (fields) => { const fieldMap = {}; fields.forEach(field => { const parts = field.split('.'); let current = fieldMap; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (i === parts.length - 1) { // Campo final current[part] = true; } else { // Campo aninhado if (!current[part]) { current[part] = {}; } current = current[part]; } } }); const buildString = (obj) => { const keys = Object.keys(obj); const fields = []; keys.forEach(key => { if (obj[key] === true) { fields.push(key); } else { fields.push(`${key} { ${buildString(obj[key])} }`); } }); return fields.join(' '); }; return buildString(fieldMap); }; const menuFields = buildGraphQLFields(fields); // Query sempre usa ID const query = ` query GetMenu($id: ID!) { menu(id: $id) { ${menuFields} } } `; const variables = { id: id }; if (this.config.debug) { console.log('[Menu] GraphQL query built:', query); console.log('[Menu] Variables:', variables); } try { const data = await this.http.graphql(query, variables); // Extrai o menu do resultado const menu = data.menu; if (!menu) { if (this.config.debug) { console.log(`[Menu] Menu not found with ID: ${id}`); } throw new Error(`Menu not found with ID: ${id}`); } // Salva no cache if (this.cache) { await this.cache.set(cacheKey, menu, this.cacheTTL); if (this.config.debug) { console.log(`[Menu] Menu cached with ID: ${id}`); } } if (this.config.debug) { console.log(`[Menu] Menu fetched successfully with ID: ${id}`); } return menu; } catch (error) { console.error(`[Menu] Error fetching menu with ID ${id}:`, error); throw error; } } /** * Busca múltiplos menus por IDs * @param {Array<string>} ids - Array de IDs dos menus (formato ULID) * @param {Array<string>} fields - Campos a buscar * @returns {Array<object>} - Array de menus */ async multiple(ids, fields = null) { if (!Array.isArray(ids) || ids.length === 0) { return []; } // Campos padrão se não especificados if (!fields) { fields = [ 'id', 'name', 'itemsCount', 'menuItems.id', 'menuItems.name', 'menuItems.link', 'menuItems.position' ]; } if (this.config.debug) { console.log(`[Menu] Getting multiple menus by IDs:`, ids); } // Busca cada menu individualmente (pode ser otimizado futuramente) const menus = []; for (const id of ids) { try { const menu = await this.get(id, fields); if (menu) { menus.push(menu); } } catch (error) { if (this.config.debug) { console.warn(`[Menu] Failed to fetch menu with ID ${id}:`, error.message); } // Continua mesmo se um menu falhar } } return menus; } /** * Busca apenas os itens de um menu específico (útil para navbars) * @param {string} id - ID do menu (formato ULID) * @param {object} options - Opções (includeChildren, maxDepth) * @returns {Array<object>} - Array de itens do menu */ async getItems(id, options = {}) { const { includeChildren = true, maxDepth = 2 } = options; const fields = [ 'menuItems.id', 'menuItems.name', 'menuItems.link', 'menuItems.position', 'menuItems.canHaveChildren' ]; if (includeChildren && maxDepth > 1) { fields.push( 'menuItems.children.id', 'menuItems.children.name', 'menuItems.children.link', 'menuItems.children.position' ); } try { const menu = await this.get(id, fields); return menu?.menuItems || []; } catch (error) { if (this.config.debug) { console.warn(`[Menu] Failed to get items for menu with ID ${id}:`, error.message); } return []; } } /** * Limpa o cache de menus */ async clearCache() { if (this.cache && this.cache.clear) { await this.cache.clear(); if (this.config.debug) { console.log('[Menu] Cache cleared'); } } } /** * Invalida cache por padrão (ex: por nome ou ID) * @param {string} pattern - Padrão para invalidar */ async invalidateCache(pattern) { if (this.cache && this.cache.invalidate) { const invalidated = this.cache.invalidate(pattern); if (this.config.debug) { console.log(`[Menu] Cache invalidated: ${invalidated} entries`); } return invalidated; } return 0; } /** * Estatísticas do cache */ async getCacheStats() { if (!this.cache || !this.cache.getStats) { return { enabled: false }; } return this.cache.getStats(); } } module.exports = Menu;