UNPKG

@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
"use strict"; 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;