@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',
    ];
}