UNPKG

gqlcheck

Version:

Performs additional checks on your GraphQL documents and operations to ensure they conform to your rules, whilst allow-listing existing operations and their constituent parts (and allowing overrides on a per-field basis). Rules include max selection set d

143 lines (142 loc) 6.77 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const node_fs_1 = require("node:fs"); const node_worker_threads_1 = require("node:worker_threads"); const graphile_config_1 = require("graphile-config"); const load_1 = require("graphile-config/load"); const graphql_1 = require("graphql"); const CountVisitor_js_1 = require("./CountVisitor.js"); const DepthVisitor_js_1 = require("./DepthVisitor.js"); const operationPaths_js_1 = require("./operationPaths.js"); const OperationPathsVisitor_js_1 = require("./OperationPathsVisitor.js"); const rulesContext_js_1 = require("./rulesContext.js"); const graphqlVersionMajor = parseInt(graphql_1.version.split(".")[0], 10); if (node_worker_threads_1.isMainThread) { throw new Error("This script is designed to be called by `scan.ts`, but it's the main thread"); } if (!node_worker_threads_1.parentPort) { throw new Error("This script is designed to be called by `scan.ts`, but there's no parent port"); } const definitelyParentPort = node_worker_threads_1.parentPort; async function main() { const { configPath, overrideConfig } = node_worker_threads_1.workerData; const rawConfig = await (0, load_1.loadConfig)(configPath); const config = (0, graphile_config_1.resolvePresets)([ rawConfig ?? {}, { gqlcheck: overrideConfig }, ]); const { gqlcheck: { schemaSdlPath = `${process.cwd()}/schema.graphql` } = {}, } = config; const middleware = new graphile_config_1.Middleware(); (0, graphile_config_1.orderedApply)(config.plugins, (p) => p.gqlcheck?.middleware, (name, fn, _plugin) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any middleware.register(name, fn); }); const schemaString = (0, node_fs_1.readFileSync)(schemaSdlPath, "utf8"); const schema = (0, graphql_1.buildASTSchema)((0, graphql_1.parse)(schemaString)); { const schemaErrors = (0, graphql_1.validateSchema)(schema); if (schemaErrors.length) { console.error(schemaErrors); throw new Error(`GraphQL schema is invalid.`); } } async function checkDocument(req) { const { sourceString, sourceName } = req; const source = new graphql_1.Source(sourceString, sourceName); const document = (0, graphql_1.parse)(source); const operationDefinitions = document.definitions.filter((o) => o.kind === graphql_1.Kind.OPERATION_DEFINITION); const typeInfo = new operationPaths_js_1.TypeAndOperationPathInfo(schema); const errors = []; function onError(error) { errors.push(error.toJSONEnhanced?.(rulesContext) ?? error.toJSON?.() ?? // Ignore deprecated, this is for GraphQL v15 support (0, graphql_1.formatError)(error)); } const rulesContext = new rulesContext_js_1.RulesContext(schema, document, typeInfo, config, onError); if (operationDefinitions.length === 0) { return { sourceName, operations: [], errors: [{ message: "Could not find any operations in this document" }], meta: rulesContext.getMeta(), filtered: 0, }; } const validationRules = [...graphql_1.specifiedRules]; const mode = graphqlVersionMajor === 15 ? 1 : graphqlVersionMajor === 16 ? 2 : 0; const validationErrors = await middleware.run("validate", { validate: graphql_1.validate, schema, document, rulesContext, validationRules, options: {}, }, ({ validate, schema, document, rulesContext, validationRules, options, }) => mode === 1 ? // GraphQL v15 style validate(schema, document, [...validationRules, () => (0, OperationPathsVisitor_js_1.OperationPathsVisitor)(rulesContext)], // eslint-disable-next-line @typescript-eslint/no-explicit-any typeInfo, // eslint-disable-next-line @typescript-eslint/no-explicit-any options) : mode === 2 ? // GraphQL v16 style validate(schema, document, [...validationRules, () => (0, OperationPathsVisitor_js_1.OperationPathsVisitor)(rulesContext)], options, rulesContext.getTypeInfo()) : // GraphQL v17 MIGHT remove typeInfo validate(schema, document, validationRules)); if (validationErrors.length > 0) { return { sourceName, operations: [], errors: validationErrors.map((e) => e.toJSON?.() ?? // Ignore deprecated, this is for GraphQL v15 support (0, graphql_1.formatError)(e)), meta: rulesContext.getMeta(), filtered: 0, }; } if (mode === 0) { // Need to revisit, because OperationPathsVisitor didn't run above. (0, graphql_1.visit)(document, (0, graphql_1.visitWithTypeInfo)(rulesContext.getTypeInfo(), (0, graphql_1.visitInParallel)([(0, OperationPathsVisitor_js_1.OperationPathsVisitor)(rulesContext)]))); } const visitors = await middleware.run("visitors", { rulesContext, visitors: [(0, CountVisitor_js_1.CountVisitor)(rulesContext), (0, DepthVisitor_js_1.DepthVisitor)(rulesContext)], }, ({ visitors }) => visitors); const visitor = await middleware.run("createVisitor", { rulesContext, visitors }, ({ rulesContext, visitors }) => (0, graphql_1.visitWithTypeInfo)(rulesContext.getTypeInfo(), (0, graphql_1.visitInParallel)(visitors))); (0, graphql_1.visit)(document, visitor); const operations = operationDefinitions.map((operationDefinition) => { const operationName = operationDefinition.name?.value; const operationKind = operationDefinition.operation; return { operationName, operationKind, }; }); return { sourceName, errors, operations, meta: rulesContext.getMeta(), filtered: 0, }; } definitelyParentPort.on("message", (req) => { if (req === "STOP") { process.exit(0); } middleware .run("checkDocument", { req }, ({ req }) => checkDocument(req)) .then((result) => { definitelyParentPort.postMessage(result); }, (e) => { console.dir(e); process.exit(1); }); }); definitelyParentPort.postMessage("READY"); } main().catch((e) => { console.error(e); process.exit(1); });