- Integrates with an Opine server in a Deno runtime.
- Enables users to customize both a maximum depth and a cost limit for all GraphQL queries and mutations sent to the server.
- Validates queries and mutations against the depth limiter and/or cost limiter before they are executed by the server.
Because GraphQL schemas can be cyclic graphs, it is possible that a client could construct a query such as this one:
Therefore, if nested deep enough, a malicious actor could potentially bring your server down with an abusive query.However, using a Depth Limiter, you can validate the depth of incoming queries against a user-defined limit and prevent these queries from going through.
Queries can still be very expensive even if they aren't nested deeply. Using a Cost Limiter, your server will calculate the total cost of the query based on its types before execution.
A set up with gql and Opine out-of-the-box:
import { opine, OpineRequest } from "https://deno.land/x/opine@2.2.0/mod.ts";
import { GraphQLHTTP } from "https://deno.land/x/gql@1.1.2/mod.ts";
import { makeExecutableSchema } from "https://deno.land/x/graphql_tools@0.0.2/mod.ts";
import { gql } from "https://deno.land/x/graphql_tag@0.0.1/mod.ts";
import { readAll } from "https://deno.land/std@0.148.0/streams/conversion.ts";
import { guarDenoQL } from "https://deno.land/x/guardenoql@v1.0.1/mod.ts";
// update GuarDenoQL import URL with most recent version
type Request = OpineRequest & { json: () => Promise<any> };
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = { Query: { hello: () => `Hello World!` } };
const dec = new TextDecoder();
const schema = makeExecutableSchema({ resolvers, typeDefs });
const app = opine();
app
.use("/graphql", async (req, res) => {
const request = req as Request;
request.json = async () => {
const rawBody = await readAll(req.raw);
const body = JSON.parse(dec.decode(rawBody));
const query = body.query;
const error = guarDenoQL(schema, query, {
depthLimitOptions: {
maxDepth: 4, // maximum depth allowed before a request is rejected
callback: (args) => console.log("query depth is:", args), // optional
},
costLimitOptions: {
maxCost: 5000, // maximum cost allowed before a request is rejected
mutationCost: 5, // cost of a mutation
objectCost: 2, // cost of retrieving an object
scalarCost: 1, // cost of retrieving a scalar
depthCostFactor: 1.5, // multiplicative cost of each depth level
callback: (args) => console.log("query cost is:", args), // optional
},
});
if (error !== undefined && !error.length) {
return body;
} else {
const errorMessage = { error };
return res.send(JSON.stringify(errorMessage));
}
};
const resp = await GraphQLHTTP<Request>({
schema,
context: (request) => ({ request }),
graphiql: true,
})(request);
for (const [k, v] of resp.headers.entries()) res.headers?.append(k, v);
res.status = resp.status;
res.send(await resp.text());
})
.listen(3000, () => console.log(`☁ Started on http://localhost:3000`));
GuarDenoQL is fully customizable.
Users can use either the depth limiter, cost limiter or both.
The first argument is the schema
, the second argument is the query
, and the
third argument is an Object
with up to two properties: depthLimitOptions
and/or costLimitOptions
.
This feature limits the depth of a document.
const error = guarDenoQL(schema, query, {
depthLimitOptions: {
maxDepth: 4, // maximum depth allowed before a request is rejected
callback: (args) => console.log("query depth is:", args), // optional
},
});
The depthLimitOptions
object has two properties to configure:
-
maxDepth
: the depth limiter will throw a validation error if the document has a greater depth than the user-suppliedmaxDepth
-
optional
callback
function: receives anObject
that maps the name of the operation to its corresponding query depth
This feature applies a cost analysis algorithm to block queries that are too expensive.
const error = guarDenoQL(schema, query, {
costLimitOptions: {
maxCost: 5000, // maximum cost allowed before a request is rejected
mutationCost: 5, // cost of a mutation
objectCost: 2, // cost of retrieving an object
scalarCost: 1, // cost of retrieving a scalar
depthCostFactor: 1.5, // multiplicative cost of each depth level
callback: (args) => console.log("query cost is:", args), // optional
},
});
The costLimitOptions
object has six properties to configure:
-
maxCost
: the cost limiter will throw a validation error if the document has a greater cost than the user-suppliedmaxCost
-
mutationCost
: represents the cost of a mutation (some popular cost analysis algorithms make mutations more expensive than queries) -
objectCost
: represents the cost of an object that has subfields -
scalarCost
: represents the cost of a scalar -
depthCostFactor
: the multiplicative cost of each depth level -
optional
callback
function: receives anObject
that maps the name of the operation to its corresponding query cost
If you would like to contribute, please see CONTRIBUTING.md for more information.
Finley Decker: GitHub | LinkedIn
Hannah McDowell: GitHub | LinkedIn
Distributed under the MIT License. See LICENSE for more information.