@quell/server
Version:
Quell is an open-source NPM package providing a light-weight caching layer implementation and cache invalidation for GraphQL responses on both the client- and server-side. Use Quell to prevent redundant client-side API requests and to minimize costly serv
123 lines (122 loc) • 6.26 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCostLimit = void 0;
const graphql_1 = require("graphql");
const quellHelpers_1 = require("../helpers/quellHelpers");
/**
* Checks the cost of the query. In the instance of a malicious or overly nested query,
* short-circuits the query before it goes to the database and passes an error with a
* status code 413 (content too large).
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
* @param {Function} next - Express next middleware function.
* @returns {void} Passes an error to Express if no query was included in the request or if the cost exceeds the maximum allowed cost.
*/
function createCostLimit(config) {
return (req, res, next) => {
var _a;
// Get the cost parameters from config
const { maxCost, mutationCost, objectCost, depthCostFactor, scalarCost } = config.costParameters;
// Get the GraphQL query string from request body.
const queryString = req.body.query;
// Pass error to Express if no query is found on the request.
if (!queryString) {
const err = {
log: "Invalid request, no query found in req.body",
status: 400,
message: {
err: "Error in QuellCache.costLimit. Check server log for more details.",
},
};
return next(err);
}
// Create the abstract syntax tree with graphql-js parser.
// If depthLimit was included before costLimit in middleware chain, we can get the AST and parsed AST from res.locals.
const AST = res.locals.AST
? res.locals.AST
: (0, graphql_1.parse)(queryString);
// Create response prototype, operation type, and fragments object.
// The response prototype is used as a template for most operations in Quell including caching, building modified requests, and more.
const { proto, operationType, frags, } = (_a = res.locals.parsedAST) !== null && _a !== void 0 ? _a : (0, quellHelpers_1.parseAST)(AST);
// Combine fragments on prototype so we can access fragment values in cache.
const prototype = Object.keys(frags).length > 0
? (0, quellHelpers_1.updateProtoWithFragment)(proto, frags)
: proto;
// Set initial cost to 0.
// If the operation is a mutation, add to the cost the mutation cost multiplied by the number of mutations.
let cost = 0;
if (operationType === "mutation") {
cost += Object.keys(prototype).length * mutationCost;
}
/**
* Helper function to pass an error if the cost of the proto is greater than the maximum cost set on server connection.
* @param {Object} proto - The prototype object to determine the cost of.
* @returns {void} Passes an error to Express if the cost of the prototype exceeds the maxCost.
*/
const determineCost = (proto) => {
// Pass error to Express if the maximum cost has been exceeded.
if (cost > maxCost) {
const err = {
log: "Error in costLimit.determineCost(helper): cost limit exceeded.",
status: 413,
message: {
err: `Cost limit exceeded, tried to send query with a cost exceeding ${maxCost}.`,
},
};
res.locals.queryErr = err;
return next(err);
}
// Loop through the fields on the prototype.
Object.keys(proto).forEach((key) => {
if (typeof proto[key] === "object" && !key.includes("__")) {
// If the current field is nested, recurse and increase the total cost by objectCost.
cost += objectCost;
return determineCost(proto[key]);
}
// If the current field is scalar, increase the total cost by the scalarCost.
if (proto[key] === true && !key.includes("__")) {
cost += scalarCost;
}
});
};
determineCost(prototype);
/**
* Helper function to pass an error if the cost of the proto, taking into account depth levels, is greater than
* the maximum cost set on server connection.
*
* This function essentially multiplies the cost by a depth cost adjustment, which is equal to the
* depthCostFactor raised to the power of the depth.
* @param {Object} proto - The prototype object to determine the cost of.
* @param {number} totalCost - Current cost of the prototype.
* @returns {void} Passes an error to Express if the cost of the prototype exceeds the maxCost.
*/
const determineDepthCost = (proto, totalCost = cost) => {
// Pass error to Express if the maximum cost has been exceeded.
if (totalCost > maxCost) {
const err = {
log: "Error in costLimit.determineDepthCost(helper): cost limit exceeded.",
status: 413,
message: {
err: `Cost limit exceeded, tried to send query with a cost exceeding ${maxCost}.`,
},
};
res.locals.queryErr = err;
return next(err);
}
// Loop through the fields, recursing and multiplying the current total cost
// by the depthCostFactor if the field is nested.
Object.keys(proto).forEach((key) => {
if (typeof proto[key] === "object" && !key.includes("__")) {
determineDepthCost(proto[key], totalCost * depthCostFactor);
}
});
};
determineDepthCost(prototype);
// Attach the AST and parsed AST to res.locals so that the next middleware doesn't need to determine these again.
res.locals.AST = AST;
res.locals.parsedAST = { proto, operationType, frags };
return next();
};
}
exports.createCostLimit = createCostLimit;