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
JavaScript
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);
});
;