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
text/typescript
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}`;
}