UNPKG

iluria-sdk

Version:

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

430 lines (370 loc) 14.7 kB
/** * GraphQL Query Builder - Constrói queries dinamicamente baseado nos campos requisitados */ class GraphQLQueryBuilder { constructor() { this.storeFields = new Set(); this.productFields = new Map(); // productId -> Set of fields this.productsFields = new Set(); this.productsFilters = {}; // Inicializar productsFilters this.blogPostsFields = new Set(); this.blogPostsFilters = {}; // Sistema simplificado para múltiplos showcases this.componentRequests = new Map(); // elementId -> {ids, fields} this.allProductIds = new Set(); // Todos IDs únicos (deduplicados) // Sistema para blogs similar ao de produtos this.blogComponentRequests = new Map(); // elementId -> {ids, fields} this.allBlogIds = new Set(); // Todos IDs de blogs únicos (deduplicados) } /** * Remove notação de array do path para GraphQL * Ex: photos[0].urls.medium -> photos.urls.medium */ cleanArrayNotation(path) { if (!path) return path; // Remove [0], [1], etc. do path return path.replace(/\[\d+\]/g, ''); } /** * Adiciona um campo à query * @param {string} path - Caminho do campo (ex: "store.branding.logoUrl", "product.name", "blog.title") * @param {object} context - Contexto adicional (productId, blogSlug, filters, etc) */ addField(path, context = {}) { if (!path) return; // Ignora expressões de template e URLs (não são campos GraphQL válidos) if (path.includes('{') || path.includes('}') || path.startsWith('/')) { return; } // Limpa notação de array do path (ex: photos[0].urls.medium -> photos.urls.medium) const cleanPath = this.cleanArrayNotation(path); // Remove prefixo store. se presente if (cleanPath.startsWith('store.')) { const field = cleanPath.substring(6); this.storeFields.add(field); } // Produto individual else if (cleanPath.startsWith('product.')) { const field = cleanPath.substring(8); const productId = context.productId; if (productId) { if (!this.productFields.has(productId)) { this.productFields.set(productId, new Set()); } this.productFields.get(productId).add(field); } } // Lista de produtos else if (cleanPath.startsWith('products.')) { const field = cleanPath.substring(9); // totalCount, hasMore, offset, limit são campos da conexão, não dos items if (field === 'totalCount' || field === 'hasMore' || field === 'offset' || field === 'limit') { // Estes campos já são incluídos automaticamente na query de conexão // Não precisamos adicioná-los aos campos dos items } else { this.productsFields.add(field); } } // Blog individual (agora usa blogPosts com mais posts para filtro local) else if (cleanPath.startsWith('blog.')) { const field = cleanPath.substring(5); const blogSlug = context.blogSlug; if (blogSlug) { // Adiciona ao blogPosts sem filtro específico this.blogPostsFields.add(field); // IMPORTANTE: Adiciona slug automaticamente para permitir filtro local this.blogPostsFields.add('slug'); // Busca mais posts para permitir filtro local this.blogPostsFilters = { ...this.blogPostsFilters, limit: 50, // Busca mais posts para filtrar localmente _individualPost: true, _slug: blogSlug }; } } // Lista de blogPosts (padrão único) else if (cleanPath.startsWith('blogPosts.')) { const field = cleanPath.substring(10); // totalCount, hasMore, offset, limit são campos da conexão, não dos items if (field === 'totalCount' || field === 'hasMore' || field === 'offset' || field === 'limit') { // Estes campos já são incluídos automaticamente na query de conexão // Não precisamos adicioná-los aos campos dos items } else { this.blogPostsFields.add(field); } // Salva filtros se fornecidos if (context.filters) { this.blogPostsFilters = { ...this.blogPostsFilters, ...context.filters }; } } // Caminho direto (assume store) else { this.storeFields.add(cleanPath); } } /** * Analisa múltiplos campos de uma vez */ addFields(fields, context = {}) { if (Array.isArray(fields)) { fields.forEach(field => this.addField(field, context)); } else if (typeof fields === 'string') { fields.split(',').forEach(field => this.addField(field.trim(), context)); } } /** * Registra um componente com seus produtos específicos * @param {string} elementId - ID único do componente * @param {Array} productIds - IDs dos produtos solicitados * @param {Array} fields - Campos solicitados */ registerComponent(elementId, productIds, fields) { if (!elementId || !productIds || productIds.length === 0) return; // Registra requisição do componente this.componentRequests.set(elementId, { ids: productIds, fields: fields || [] }); // Adiciona IDs ao conjunto global (deduplica automaticamente) productIds.forEach(id => this.allProductIds.add(id)); // Adiciona campos aos campos comuns fields.forEach(field => this.productsFields.add(field)); } /** * Registra um componente de blog com seus posts específicos * @param {string} elementId - ID único do componente * @param {Array} blogIds - IDs dos blog posts solicitados * @param {Array} fields - Campos solicitados */ registerBlogComponent(elementId, blogIds, fields) { if (!elementId || !blogIds || blogIds.length === 0) return; // Registra requisição do componente de blog this.blogComponentRequests.set(elementId, { ids: blogIds, fields: fields || [] }); // Adiciona IDs ao conjunto global de blogs (deduplica automaticamente) blogIds.forEach(id => this.allBlogIds.add(id)); // Adiciona campos aos campos comuns de blogs fields.forEach(field => this.blogPostsFields.add(field)); } /** * Constrói a estrutura de campos para GraphQL * @param {Set|Array} fields - Campos a incluir * @returns {string} - Estrutura de campos GraphQL */ buildFieldStructure(fields) { const fieldArray = Array.from(fields); const structure = {}; // Organiza campos em estrutura aninhada fieldArray.forEach(field => { const parts = field.split('.'); let current = structure; parts.forEach((part, index) => { if (index === parts.length - 1) { // Último elemento - campo simples current[part] = true; } else { // Elemento intermediário - objeto aninhado if (!current[part]) { current[part] = {}; } current = current[part]; } }); }); return this.structureToGraphQL(structure); } /** * Converte estrutura de objetos em sintaxe GraphQL */ structureToGraphQL(structure, indent = '') { const fields = []; Object.entries(structure).forEach(([key, value]) => { if (value === true) { // Campo simples fields.push(key); } else if (typeof value === 'object') { // Campo com sub-campos const subFields = this.structureToGraphQL(value, indent + ' '); fields.push(`${key} { ${subFields} }`); } }); return fields.join(' '); } /** * Constrói a query GraphQL completa * @returns {string} - Query GraphQL */ buildQuery() { const queryParts = []; const variables = []; // Declarar variables aqui para evitar erro // StoreData query if (this.storeFields.size > 0) { const fields = this.buildFieldStructure(this.storeFields); queryParts.push(`storeData { ${fields} }`); } // Query única de produtos com todos IDs combinados if (this.allProductIds.size > 0 && this.productsFields.size > 0) { const fields = this.buildFieldStructure(this.productsFields); const idsArray = Array.from(this.allProductIds); // Query simples com IDs inline (sem variáveis para evitar perda de contexto) queryParts.push(`products(ids: [${idsArray.map(id => `"${id}"`).join(', ')}]) { items { ${fields} } totalCount hasMore offset limit }`); } // Fallback: Products list query sem grupos (compatibilidade) else if (this.productsFields.size > 0) { // Adiciona variáveis para filtros const productsVars = []; const varDefinitions = []; if (this.productsFilters.limit !== undefined) { productsVars.push('limit: $productsLimit'); varDefinitions.push('$productsLimit: Int'); } if (this.productsFilters.offset !== undefined) { productsVars.push('offset: $productsOffset'); varDefinitions.push('$productsOffset: Int'); } if (this.productsFilters.search) { productsVars.push('search: $productsSearch'); varDefinitions.push('$productsSearch: String'); } if (this.productsFilters.categoryId) { productsVars.push('categoryId: $productsCategoryId'); varDefinitions.push('$productsCategoryId: String'); } if (this.productsFilters.type) { productsVars.push('type: $productsType'); varDefinitions.push('$productsType: ProductType'); } if (this.productsFilters.orderBy) { productsVars.push('orderBy: $productsOrderBy'); varDefinitions.push('$productsOrderBy: ProductOrderBy'); } // Suporte para filtro por IDs específicos if (this.productsFilters.ids && this.productsFilters.ids.length > 0) { productsVars.push('ids: $productsIds'); varDefinitions.push('$productsIds: [String!]'); } const fields = this.buildFieldStructure(this.productsFields); const varsString = productsVars.length > 0 ? `(${productsVars.join(', ')})` : ''; queryParts.push(`products${varsString} { items { ${fields} } totalCount hasMore offset limit }`); variables.push(...varDefinitions); } // Query única de blogs com todos IDs combinados (similar aos produtos) if (this.allBlogIds.size > 0 && this.blogPostsFields.size > 0) { const fields = this.buildFieldStructure(this.blogPostsFields); const idsArray = Array.from(this.allBlogIds); // Query simples com IDs inline (sem variáveis para evitar perda de contexto) queryParts.push(`blogPosts(ids: [${idsArray.map(id => `"${id}"`).join(', ')}]) { items { ${fields} } totalCount hasMore offset limit }`); } // Fallback: BlogPosts list query sem IDs específicos (compatibilidade) else if (this.blogPostsFields.size > 0) { // Adiciona variáveis para filtros const blogPostsVars = []; const varDefinitions = []; if (this.blogPostsFilters.limit !== undefined) { blogPostsVars.push('limit: $blogPostsLimit'); varDefinitions.push('$blogPostsLimit: Int'); } if (this.blogPostsFilters.offset !== undefined) { blogPostsVars.push('offset: $blogPostsOffset'); varDefinitions.push('$blogPostsOffset: Int'); } // Removido filtro search - agora filtramos localmente if (this.blogPostsFilters.categoryId) { blogPostsVars.push('categoryId: $blogPostsCategoryId'); varDefinitions.push('$blogPostsCategoryId: String'); } const fields = this.buildFieldStructure(this.blogPostsFields); const varsString = blogPostsVars.length > 0 ? `(${blogPostsVars.join(', ')})` : ''; queryParts.push(`blogPosts${varsString} { items { ${fields} } totalCount hasMore offset limit }`); variables.push(...varDefinitions); } // Individual product queries this.productFields.forEach((fields, productId) => { const fieldStructure = this.buildFieldStructure(fields); const alias = `product_${productId.replace(/[^a-zA-Z0-9]/g, '_')}`; queryParts.push(`${alias}: product(id: "${productId}") { ${fieldStructure} }`); }); // Blog individual não precisa mais de query separada // Agora usa blogPosts com filtro de slug // Constrói query final sem variáveis (tudo inline) if (queryParts.length === 0) { return null; } return `query { ${queryParts.join(' ')} }`; } /** * Constrói variáveis para a query * @returns {object} - Variáveis GraphQL */ getVariables() { const variables = {}; // Sem variáveis para produtos - tudo inline agora // Mantém apenas para outras queries que ainda usam variáveis // Adiciona variáveis de blogPosts com prefixo if (this.blogPostsFilters.limit !== undefined) { variables.blogPostsLimit = this.blogPostsFilters.limit; } if (this.blogPostsFilters.offset !== undefined) { variables.blogPostsOffset = this.blogPostsFilters.offset; } // Removido blogPostsSearch - agora filtramos localmente if (this.blogPostsFilters.categoryId) { variables.blogPostsCategoryId = this.blogPostsFilters.categoryId; } return variables; } /** * Limpa todos os campos */ clear() { this.storeFields.clear(); this.productFields.clear(); this.productsFields.clear(); this.productsFilters = {}; // Limpar productsFilters this.blogPostsFields.clear(); this.blogPostsFilters = {}; // Limpa sistema de componentes this.componentRequests.clear(); this.allProductIds.clear(); // Limpa sistema de blogs this.blogComponentRequests.clear(); this.allBlogIds.clear(); } /** * Verifica se tem campos para buscar */ hasFields() { return this.storeFields.size > 0 || this.productFields.size > 0 || this.productsFields.size > 0 || this.blogPostsFields.size > 0; } } module.exports = GraphQLQueryBuilder;