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