UNPKG

graphql-lint-clint-platform

Version:

GraphQL unused fields linter for Clint platform - Custom patterns and actions.graphql support

230 lines (188 loc) 7.02 kB
import { GraphQLQuery, GraphQLField } from "@graphql-lint/core"; import * as fs from "fs"; import * as path from "path"; import { parse as parseGraphQL, DocumentNode, DefinitionNode, FieldDefinitionNode } from "graphql"; export class ClintGraphQLExtractor { private actionsQueries: Map<string, GraphQLQuery> = new Map(); /** * Detecta queries customizadas da Clint baseadas em actions.graphql */ async extractClintQueries(projectPath: string): Promise<GraphQLQuery[]> { const queries: GraphQLQuery[] = []; // Extrair queries do actions.graphql da Clint const actionsQueries = await this.extractFromActionsGraphQL(projectPath); queries.push(...actionsQueries); return queries; } /** * Extrai queries do arquivo actions.graphql da Clint */ private async extractFromActionsGraphQL(projectPath: string): Promise<GraphQLQuery[]> { const actionsFiles = this.findActionsFiles(projectPath); const queries: GraphQLQuery[] = []; for (const actionsFile of actionsFiles) { try { const content = fs.readFileSync(actionsFile, 'utf-8'); const document = parseGraphQL(content); const actionsQueries = this.parseActionsDocument(document, actionsFile); queries.push(...actionsQueries); // Armazenar para mapeamento posterior actionsQueries.forEach(query => { this.actionsQueries.set(query.name, query); }); } catch (error) { console.warn(`⚠️ Erro ao processar actions.graphql em ${actionsFile}:`, error); } } return queries; } /** * Procura arquivos actions.graphql no projeto */ private findActionsFiles(projectPath: string): string[] { const actionsFiles: string[] = []; const searchPaths = [ path.join(projectPath, 'actions.graphql'), path.join(projectPath, 'hasura', 'actions.graphql'), path.join(projectPath, 'graphql', 'actions.graphql'), path.join(projectPath, 'schema', 'actions.graphql'), ]; // Busca recursiva por actions.graphql const findRecursive = (dir: string) => { try { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory() && !item.includes('node_modules')) { findRecursive(fullPath); } else if (item === 'actions.graphql') { actionsFiles.push(fullPath); } } } catch (error) { // Ignorar diretórios inacessíveis } }; // Verificar caminhos conhecidos searchPaths.forEach(filePath => { if (fs.existsSync(filePath)) { actionsFiles.push(filePath); } }); // Busca recursiva a partir da raiz findRecursive(projectPath); return [...new Set(actionsFiles)]; // Remove duplicatas } /** * Parseia o documento GraphQL do actions.graphql */ private parseActionsDocument(document: DocumentNode, filePath: string): GraphQLQuery[] { const queries: GraphQLQuery[] = []; for (const definition of document.definitions) { if (definition.kind === 'ObjectTypeDefinition' && definition.name.value === 'Query') { // Processar campos da Query type if (definition.fields) { for (const field of definition.fields) { const query = this.parseActionField(field, filePath); if (query) { queries.push(query); } } } } } return queries; } /** * Converte um campo de action em GraphQLQuery * owner_get_name(id: uuid!) -> owner_get_name query */ private parseActionField(field: FieldDefinitionNode, filePath: string): GraphQLQuery | null { try { const queryName = field.name.value; // Extrair campos do tipo de retorno (se disponível) const fields = this.extractFieldsFromActionReturnType(field, queryName); return { name: queryName, fields: fields, rawQuery: `# Action: ${queryName}`, location: { file: filePath, line: field.loc?.startToken?.line || 0, column: field.loc?.startToken?.column || 0 } }; } catch (error) { console.warn(`⚠️ Erro ao processar action field ${field.name.value}:`, error); return null; } } /** * Extrai campos baseados no tipo de retorno da action */ private extractFieldsFromActionReturnType(field: FieldDefinitionNode, queryName: string): GraphQLField[] { // Por enquanto, criar campos sintéticos baseados no nome da action // Em uma implementação mais completa, poderia analisar o schema completo const fields: GraphQLField[] = []; // Padrão: owner_get_name -> campos relacionados a owner const entityMatch = queryName.match(/^(\w+)_/); if (entityMatch) { const entity = entityMatch[1]; // Campos comuns baseados na entidade const commonFields = this.getCommonFieldsForEntity(entity); fields.push(...commonFields); } // Adicionar campo de status padrão para actions fields.push({ name: 'status', path: ['status'], line: field.loc?.startToken?.line || 0, column: field.loc?.startToken?.column || 0 }); return fields; } /** * Gera campos comuns para uma entidade */ private getCommonFieldsForEntity(entity: string): GraphQLField[] { const commonFieldsByEntity: Record<string, string[]> = { owner: ['id', 'name', 'email', 'type'], user: ['id', 'name', 'email', 'avatar'], payment: ['id', 'amount', 'status', 'method'], order: ['id', 'total', 'status', 'items'], product: ['id', 'name', 'price', 'category'], }; const fieldNames = commonFieldsByEntity[entity] || ['id', 'name']; return fieldNames.map((fieldName, index) => ({ name: fieldName, path: [fieldName], line: 0, column: index * 10 })); } } /** * Converte nome de action para padrão Clint * owner_get_name -> clint.owner.getName */ export function actionToClintPattern(actionName: string): string | null { const match = actionName.match(/^(\w+)_(.+)$/); if (!match) return null; const [, entity, action] = match; // Converter action para camelCase const camelAction = action.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); return `clint.${entity}.${camelAction}`; } /** * Converte padrão Clint para nome de action * clint.owner.getName -> owner_get_name */ export function clintPatternToAction(clintPattern: string): string | null { const match = clintPattern.match(/^clint\.(\w+)\.(.+)$/); if (!match) return null; const [, entity, method] = match; // Converter camelCase para snake_case const snakeAction = method.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); return `${entity}_${snakeAction}`; }