@graphql-inspector/core
Version:
Tooling for GraphQL. Compare GraphQL Schemas, check documents, find breaking changes, find similar types.
157 lines (156 loc) • 6.14 kB
JavaScript
import { isInterfaceType, isObjectType, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
import { readDocument } from '../ast/document.js';
import { isForIntrospection, isPrimitive } from '../utils/graphql.js';
export function coverage(schema, sources) {
const coverage = {
sources,
types: {},
stats: {
numTypes: 0,
numTypesCoveredFully: 0,
numTypesCovered: 0,
numFields: 0,
numFieldsCovered: 0,
numFiledsCovered: 0,
numQueries: 0,
numCoveredQueries: 0,
numMutations: 0,
numCoveredMutations: 0,
numSubscriptions: 0,
numCoveredSubscriptions: 0,
},
};
const typeMap = schema.getTypeMap();
const typeInfo = new TypeInfo(schema);
const visitor = source => ({
Field(node) {
const fieldDef = typeInfo.getFieldDef();
const parent = typeInfo.getParentType();
if (parent?.name &&
!isForIntrospection(parent.name) &&
fieldDef?.name &&
fieldDef.name !== '__typename' &&
fieldDef.name !== '__schema') {
const sourceName = source.name;
const typeCoverage = coverage.types[parent.name];
const fieldCoverage = typeCoverage.children[fieldDef.name];
const locations = fieldCoverage.locations[sourceName];
switch (typeCoverage.type.name) {
case 'Query':
coverage.stats.numCoveredQueries++;
break;
case 'Mutation':
coverage.stats.numCoveredMutations++;
break;
case 'Subscription':
coverage.stats.numCoveredSubscriptions++;
break;
}
typeCoverage.hits++;
fieldCoverage.hits++;
if (node.loc) {
fieldCoverage.locations[sourceName] = [node.loc, ...(locations || [])];
}
if (node.arguments) {
for (const argNode of node.arguments) {
const argCoverage = fieldCoverage.children[argNode.name.value];
argCoverage.hits++;
if (argNode.loc) {
argCoverage.locations[sourceName] = [
argNode.loc,
...(argCoverage.locations[sourceName] || []),
];
}
}
}
}
},
});
for (const typename in typeMap) {
if (!isForIntrospection(typename) && !isPrimitive(typename)) {
const type = typeMap[typename];
if (isObjectType(type) || isInterfaceType(type)) {
const typeCoverage = {
hits: 0,
fieldsCount: 0,
fieldsCountCovered: 0,
type,
children: {},
};
const fieldMap = type.getFields();
for (const fieldname in fieldMap) {
if (isObjectType(type) || isInterfaceType(type)) {
switch (type.name) {
case 'Query':
coverage.stats.numQueries++;
break;
case 'Mutation':
coverage.stats.numMutations++;
break;
case 'Subscription':
coverage.stats.numSubscriptions++;
break;
}
}
const field = fieldMap[fieldname];
typeCoverage.children[field.name] = {
hits: 0,
fieldsCount: 0,
fieldsCountCovered: 0,
locations: {},
children: {},
};
for (const arg of field.args) {
typeCoverage.children[field.name].children[arg.name] = {
hits: 0,
fieldsCount: 0,
fieldsCountCovered: 0,
locations: {},
};
}
}
coverage.types[type.name] = typeCoverage;
}
}
}
const documents = coverage.sources.map(readDocument);
for (const [i, doc] of documents.entries()) {
const source = coverage.sources[i];
for (const op of doc.operations) {
visit(op.node, visitWithTypeInfo(typeInfo, visitor(source)));
}
for (const fr of doc.fragments) {
visit(fr.node, visitWithTypeInfo(typeInfo, visitor(source)));
}
}
for (const key in coverage.types) {
const me = coverage.types[key];
processStats(me);
coverage.stats.numTypes++;
if (me.fieldsCountCovered > 0)
coverage.stats.numTypesCovered++;
if (me.fieldsCount === me.fieldsCountCovered)
coverage.stats.numTypesCoveredFully++;
coverage.stats.numFields += me.fieldsCount;
coverage.stats.numFieldsCovered += me.fieldsCountCovered;
coverage.stats.numFiledsCovered = coverage.stats.numFieldsCovered;
}
return coverage;
}
function processStats(me) {
const children = me.children;
if (children) {
for (const k in children) {
const ch = children[k];
if (ch.children !== undefined) {
processStats(ch);
me.fieldsCount += ch.fieldsCount;
me.fieldsCountCovered += ch.fieldsCountCovered;
}
me.fieldsCount++;
if (ch.hits > 0) {
me.fieldsCountCovered++;
}
}
}
}