Skip to content

Instantly share code, notes, and snippets.

@RStankov
Created September 23, 2024 13:12
Show Gist options
  • Save RStankov/6577a9101fbfa19079c0611d910f7a7c to your computer and use it in GitHub Desktop.
Save RStankov/6577a9101fbfa19079c0611d910f7a7c to your computer and use it in GitHub Desktop.
Detects unused GraphQL fields
import * as glob from 'glob';
import difference from 'lodash/difference';
import startCase from 'lodash/startCase';
import union from 'lodash/union';
import uniq from 'lodash/uniq';
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
import { DocumentNode, DefinitionNode, OperationDefinitionNode } from 'graphql';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { loadDocuments } from '@graphql-tools/load';
import { visit } from 'graphql/language/visitor';
type IOptions = {
schema: ISchema;
ignore?: string[];
pattern?: string;
};
export default async function detectUnusedFields({
schema,
ignore = ['**/node_modules/**'],
pattern = '**/*.graphql',
}: IOptions): Promise<string[]> {
const fields = getSchemaFields(schema);
const files = glob.sync(pattern, {
ignore,
realpath: true,
});
const operations = await Promise.all(files.map(operationsFormFile));
return difference(
Object.keys(fields),
replaceOperationWithFields(union(...operations), fields),
);
}
function getSchemaFields(schema: ISchema): ITypesMap {
return schema.__schema.types.reduce((acc, schemaType) => {
if (schemaType.kind !== 'OBJECT' || schemaType.name.startsWith('__')) {
return acc;
}
(schemaType.fields || []).forEach((field) => {
let type = field.type;
while (type.ofType) {
type = type.ofType;
}
acc[`${schemaType.name}.${field.name}`] = type.name;
});
return acc;
}, {});
}
async function operationsFormFile(file: string): Promise<string[]> {
const sources = await loadDocuments(file, {
loaders: [new CodeFileLoader(), new GraphQLFileLoader()],
});
return sources.reduce((acc, { document }) => {
if (document === undefined) {
return acc;
}
return acc.concat(extractOperations(document));
}, [] as string[]);
}
function extractOperations(document: DocumentNode): string[] {
const operation = document.definitions.find(
(node: DefinitionNode): node is OperationDefinitionNode =>
node.kind === 'OperationDefinition',
)?.operation;
if (!operation) {
return [];
}
const paths: string[] = [];
let currentPath: string[] = [];
let currentScope = startCase(operation);
let previousPath: string[] = [];
let previousScope = '';
visit(document, {
enter(node) {
switch (node.kind) {
case 'Field':
currentPath.push(node.name.value);
paths.push(`${currentScope}.${currentPath.join('.')}`);
break;
case 'FragmentDefinition':
currentScope = node.typeCondition?.name?.value as string;
currentPath = [];
break;
case 'InlineFragment':
previousScope = currentScope;
previousPath = currentPath;
paths.push(`${currentScope}.${currentPath.join('.')}`);
currentScope = node.typeCondition?.name?.value as string;
currentPath = [];
break;
default:
break;
}
},
leave(node) {
switch (node.kind) {
case 'Field':
currentPath.pop();
break;
case 'FragmentDefinition':
currentScope = startCase(operation);
break;
case 'InlineFragment':
currentPath = previousPath;
currentScope = previousScope;
break;
default:
break;
}
},
});
return uniq(paths);
}
function replaceOperationWithFields(paths: string[], typesMap: ITypesMap): string[] {
return paths.map((path) => {
const parts = path.split('.');
if (parts.length < 3) {
return path;
}
let head = parts.shift();
while (parts.length > 1) {
head = typesMap[`${head}.${parts.shift()}`];
}
return `${head}.${parts.shift()}`;
});
}
type ITypesMap = Record<string, string>;
type ISchema = {
__schema: {
types: ISchemaType[];
};
};
type ISchemaType = {
name: string;
kind: ISchemaTypeKind;
fields: ISchemaField[];
};
type ISchemaField = {
name: string;
type: ISchemaFieldType;
};
type ISchemaFieldType = {
name: string;
kind: ISchemaTypeKind;
ofType: ISchemaFieldType | null;
};
type ISchemaTypeKind =
| 'OBJECT'
| 'INTERFACE'
| 'UNION'
| 'ENUM'
| 'INPUT_OBJECT'
| 'SCALAR'
| 'LIST'
| 'NON_NULL';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment