@graphql-inspector/core
Version:
Tooling for GraphQL. Compare GraphQL Schemas, check documents, find breaking changes, find similar types.
165 lines (164 loc) • 6.75 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validate = validate;
const dependency_graph_1 = require("dependency-graph");
const graphql_1 = require("graphql");
const document_js_1 = require("../ast/document.js");
const apollo_js_1 = require("../utils/apollo.js");
const graphql_js_1 = require("../utils/graphql.js");
const alias_count_js_1 = require("./alias-count.js");
const complexity_js_1 = require("./complexity.js");
const directive_count_js_1 = require("./directive-count.js");
const query_depth_js_1 = require("./query-depth.js");
const token_count_js_1 = require("./token-count.js");
function validate(schema, sources, options) {
const config = {
strictDeprecated: true,
strictFragments: true,
keepClientFields: false,
apollo: false,
...options,
};
const invalidDocuments = [];
// read documents
const documents = sources.map(document_js_1.readDocument);
// keep all named fragments
const fragments = [];
const fragmentNames = [];
const graph = new dependency_graph_1.DepGraph({ circular: true });
for (const doc of documents) {
for (const fragment of doc.fragments) {
fragmentNames.push(fragment.node.name.value);
fragments.push(fragment);
graph.addNode(fragment.node.name.value, fragment.node);
}
}
for (const fragment of fragments) {
const depends = extractFragments((0, graphql_1.print)(fragment.node));
if (depends) {
for (const name of depends) {
graph.addDependency(fragment.node.name.value, name);
}
}
}
for (const doc of documents
// since we include fragments, validate only operations
.filter(doc => doc.hasOperations)) {
const docWithOperations = {
kind: graphql_1.Kind.DOCUMENT,
definitions: doc.operations.map(d => d.node),
};
const extractedFragments = (extractFragments((0, graphql_1.print)(docWithOperations)) || [])
// resolve all nested fragments
.map(fragmentName => resolveFragment(graph.getNodeData(fragmentName), graph))
// flatten arrays
.reduce((list, current) => list.concat(current), [])
// remove duplicates
.filter((def, i, all) => all.findIndex(item => item.name.value === def.name.value) === i);
const merged = {
kind: graphql_1.Kind.DOCUMENT,
definitions: [...docWithOperations.definitions, ...extractedFragments],
};
const transformedSchema = config.apollo ? (0, apollo_js_1.transformSchemaWithApollo)(schema) : schema;
const transformedDoc = config.apollo
? (0, apollo_js_1.transformDocumentWithApollo)(merged, {
keepClientFields: config.keepClientFields,
})
: merged;
const errors = (0, graphql_1.validate)(transformedSchema, transformedDoc) || [];
if (config.maxDepth) {
const depthError = (0, query_depth_js_1.validateQueryDepth)({
source: doc.source,
doc: transformedDoc,
maxDepth: config.maxDepth,
fragmentGraph: graph,
});
if (depthError) {
errors.push(depthError);
}
}
if (config.validateComplexityConfig) {
const complexityScoreError = (0, complexity_js_1.validateComplexity)({
source: doc.source,
doc: transformedDoc,
maxComplexityScore: config.validateComplexityConfig.maxComplexityScore,
config: {
scalarCost: config.validateComplexityConfig.complexityScalarCost,
objectCost: config.validateComplexityConfig.complexityObjectCost,
depthCostFactor: config.validateComplexityConfig.complexityDepthCostFactor,
},
fragmentGraph: graph,
});
if (complexityScoreError) {
errors.push(complexityScoreError);
}
}
if (config.maxAliasCount) {
const aliasError = (0, alias_count_js_1.validateAliasCount)({
source: doc.source,
doc: transformedDoc,
maxAliasCount: config.maxAliasCount,
fragmentGraph: graph,
});
if (aliasError) {
errors.push(aliasError);
}
}
if (config.maxDirectiveCount) {
const directiveError = (0, directive_count_js_1.validateDirectiveCount)({
source: doc.source,
doc: transformedDoc,
maxDirectiveCount: config.maxDirectiveCount,
fragmentGraph: graph,
});
if (directiveError) {
errors.push(directiveError);
}
}
if (config.maxTokenCount) {
const tokenCountError = (0, token_count_js_1.validateTokenCount)({
source: doc.source,
document: transformedDoc,
maxTokenCount: config.maxTokenCount,
getReferencedFragmentSource: fragmentName => (0, graphql_1.print)(graph.getNodeData(fragmentName)),
});
if (tokenCountError) {
errors.push(tokenCountError);
}
}
const deprecated = config.strictDeprecated
? (0, graphql_js_1.findDeprecatedUsages)(transformedSchema, transformedDoc)
: [];
const duplicatedFragments = config.strictFragments
? findDuplicatedFragments(fragmentNames)
: [];
if (sumLengths(errors, duplicatedFragments, deprecated) > 0) {
invalidDocuments.push({
source: doc.source,
errors: [...errors, ...duplicatedFragments],
deprecated,
});
}
}
return invalidDocuments;
}
function findDuplicatedFragments(fragmentNames) {
return fragmentNames
.filter((name, i, all) => all.indexOf(name) !== i)
.map(name => new graphql_1.GraphQLError(`Name of '${name}' fragment is not unique`));
}
//
// PostInfo -> AuthorInfo
// AuthorInfo -> None
//
function resolveFragment(fragment, graph) {
return graph
.dependenciesOf(fragment.name.value)
.reduce((list, current) => [...list, ...resolveFragment(graph.getNodeData(current), graph)], [fragment]);
}
function extractFragments(document) {
return (document.match(/[.]{3}[a-z0-9_]+\b/gi) || []).map(name => name.replace('...', ''));
}
function sumLengths(...arrays) {
return arrays.reduce((sum, { length }) => sum + length, 0);
}