@graphql-inspector/validate-command
Version:
Validate Documents in GraphQL Inspector
273 lines (272 loc) • 10.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handler = handler;
const fs_1 = require("fs");
const path_1 = require("path");
const graphql_1 = require("graphql");
const commands_1 = require("@graphql-inspector/commands");
const core_1 = require("@graphql-inspector/core");
const logger_1 = require("@graphql-inspector/logger");
function handler({ schema, documents, strictFragments, maxDepth, maxDirectiveCount, maxAliasCount, maxTokenCount, apollo, keepClientFields, failOnDeprecated, filter, onlyErrors, relativePaths, output, silent, validateComplexityConfig, }) {
let invalidDocuments = (0, core_1.validate)(schema, documents.map(doc => new graphql_1.Source((0, graphql_1.print)(doc.document), doc.location)), {
strictFragments,
maxDepth,
maxAliasCount,
maxDirectiveCount,
maxTokenCount,
apollo,
keepClientFields,
validateComplexityConfig,
});
if (!invalidDocuments.length) {
logger_1.Logger.success('All documents are valid');
return;
}
if (failOnDeprecated) {
invalidDocuments = moveDeprecatedToErrors(invalidDocuments);
}
if (relativePaths) {
invalidDocuments = useRelativePaths(invalidDocuments);
}
const errorsCount = countErrors(invalidDocuments);
const deprecated = countDeprecated(invalidDocuments);
const shouldFailProcess = errorsCount > 0;
if (errorsCount) {
if (!silent) {
logger_1.Logger.log(`\nDetected ${errorsCount} invalid document${errorsCount > 1 ? 's' : ''}:\n`);
}
printInvalidDocuments(useFilter(invalidDocuments, filter), 'errors', true, silent);
}
else {
logger_1.Logger.success('All documents are valid');
}
if (deprecated && !onlyErrors) {
if (!silent) {
logger_1.Logger.info(`\nDetected ${deprecated} document${deprecated > 1 ? 's' : ''} with deprecated fields:\n`);
}
printInvalidDocuments(useFilter(invalidDocuments, filter), 'deprecated', false, silent);
}
if (output) {
(0, fs_1.writeFileSync)(output, JSON.stringify({
status: !shouldFailProcess,
documents: useFilter(invalidDocuments, filter),
}, null, 2), 'utf8');
}
if (shouldFailProcess) {
process.exit(1);
}
}
function moveDeprecatedToErrors(docs) {
return docs.map(doc => ({
source: doc.source,
errors: [...(doc.errors ?? []), ...(doc.deprecated ?? [])],
deprecated: [],
}));
}
function useRelativePaths(docs) {
return docs.map(doc => {
doc.source.name = (0, path_1.relative)(process.cwd(), doc.source.name);
return doc;
});
}
function useFilter(docs, patterns) {
if (!patterns?.length) {
return docs;
}
return docs.filter(doc => patterns.some(filepath => doc.source.name.includes(filepath)));
}
exports.default = (0, commands_1.createCommand)(api => {
const { loaders } = api;
return {
command: 'validate <documents> <schema>',
describe: 'Validate Fragments and Operations',
builder(yargs) {
return yargs
.positional('schema', {
describe: 'Point to a schema',
type: 'string',
demandOption: true,
})
.positional('documents', {
describe: 'Point to documents',
type: 'string',
demandOption: true,
})
.options({
deprecated: {
alias: 'd',
describe: 'Fail on deprecated usage',
type: 'boolean',
default: false,
},
noStrictFragments: {
describe: 'Do not fail on duplicated fragment names',
type: 'boolean',
default: false,
},
maxDepth: {
describe: 'Fail on deep operations',
type: 'number',
},
maxAliasCount: {
describe: 'Fail on operations with too many aliases',
type: 'number',
},
maxDirectiveCount: {
describe: 'Fail on operations with too many directives',
type: 'number',
},
maxTokenCount: {
describe: 'Fail on operations with too many tokens',
type: 'number',
},
apollo: {
describe: 'Support Apollo directives',
type: 'boolean',
default: false,
},
keepClientFields: {
describe: 'Keeps the fields with @client, but removes @client directive from them',
type: 'boolean',
default: false,
},
filter: {
describe: 'Show results only from a list of files (or file)',
array: true,
type: 'string',
},
ignore: {
describe: 'Ignore and do not load these files (supports glob)',
array: true,
type: 'string',
},
onlyErrors: {
describe: 'Show only errors',
type: 'boolean',
default: false,
},
relativePaths: {
describe: 'Show relative paths',
type: 'boolean',
default: false,
},
silent: {
describe: 'Do not print results',
type: 'boolean',
default: false,
},
output: {
describe: 'Output JSON file',
type: 'string',
},
maxComplexityScore: {
describe: 'Fail on complexity score operations',
type: 'number',
},
complexityScalarCost: {
describe: 'Scalar cost config to use with maxComplexityScore',
type: 'number',
default: 1,
},
complexityObjectCost: {
describe: 'Object cost config to use with maxComplexityScore',
type: 'number',
default: 2,
},
complexityDepthCostFactor: {
describe: 'Depth cost factor config to use with maxComplexityScore',
type: 'number',
default: 1.5,
},
});
},
async handler(args) {
const { headers, token } = (0, commands_1.parseGlobalArgs)(args);
const apollo = args.apollo || false;
const aws = args.aws || false;
const apolloFederation = args.federation || false;
const method = args.method?.toUpperCase() || 'POST';
const maxDepth = args.maxDepth == null ? undefined : args.maxDepth;
const maxAliasCount = args.maxAliasCount == null ? undefined : args.maxAliasCount;
const maxDirectiveCount = args.maxDirectiveCount == null ? undefined : args.maxDirectiveCount;
const maxTokenCount = args.maxTokenCount == null ? undefined : args.maxTokenCount;
const strictFragments = !args.noStrictFragments;
const keepClientFields = args.keepClientFields || false;
const failOnDeprecated = args.deprecated;
const output = args.output;
const silent = args.silent || false;
const relativePaths = args.relativePaths || false;
const onlyErrors = args.onlyErrors || false;
const ignore = args.ignore || [];
const validateComplexityConfig = (() => {
if (args.maxComplexityScore == null)
return;
return {
maxComplexityScore: args.maxComplexityScore,
complexityScalarCost: args.complexityScalarCost,
complexityObjectCost: args.complexityObjectCost,
complexityDepthCostFactor: args.complexityDepthCostFactor,
};
})();
const schema = await loaders.loadSchema(args.schema, {
headers,
token,
method,
}, apolloFederation, aws);
const documents = await loaders.loadDocuments(args.documents, {
ignore,
});
return handler({
schema,
documents,
apollo,
maxDepth,
maxAliasCount,
maxDirectiveCount,
maxTokenCount,
strictFragments,
keepClientFields,
failOnDeprecated,
filter: args.filter,
silent,
output,
relativePaths,
onlyErrors,
validateComplexityConfig,
});
},
};
});
function countErrors(invalidDocuments) {
if (invalidDocuments.length) {
return invalidDocuments.filter(doc => doc.errors?.length).length;
}
return 0;
}
function countDeprecated(invalidDocuments) {
if (invalidDocuments.length) {
return invalidDocuments.filter(doc => doc.deprecated?.length).length;
}
return 0;
}
function printInvalidDocuments(invalidDocuments, listKey, isError = false, silent = false) {
if (silent) {
return;
}
for (const doc of invalidDocuments) {
if (doc.errors.length) {
for (const line of renderErrors(doc.source.name, doc[listKey], isError)) {
logger_1.Logger.log(line);
}
}
}
}
function renderErrors(sourceName, errors, isError = false) {
const errorsAsString = errors.map(e => ` - ${(0, logger_1.bolderize)(e.message)}`).join('\n');
return [
isError ? logger_1.chalk.redBright('error') : logger_1.chalk.yellowBright('warn'),
`in ${sourceName}:\n\n`,
errorsAsString,
'\n\n',
];
}