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
JavaScript
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;