Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/plugins/cost-limit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class CostLimitVisitor {

onOperationDefinitionEnter(operation: OperationDefinitionNode): void {
const complexity = this.computeComplexity(operation);

if (complexity > this.config.maxCost) {
const message = this.config.exposeLimits
? `Query Cost limit of ${this.config.maxCost} exceeded, found ${complexity}.`
Expand Down Expand Up @@ -101,6 +102,19 @@ class CostLimitVisitor {
let cost = this.config.scalarCost;
if ('selectionSet' in node && node.selectionSet) {
cost = this.config.objectCost;

let setMultiplier = 1;
if ('arguments' in node && node.arguments) {
for (const arg of node.arguments) {
if (arg.name.value === 'first' || arg.name.value === 'last') {
if (arg.value.kind === 'IntValue') {
setMultiplier = Math.max(parseInt(arg.value.value, 10) ?? 1, setMultiplier);
}
break;
}
}
}

for (const child of node.selectionSet.selections) {
if (
this.config.flattenFragments &&
Expand All @@ -111,6 +125,8 @@ class CostLimitVisitor {
cost += this.config.depthCostFactor * this.computeComplexity(child, depth + 1);
}
}
// Apply setMultiplier to the total cost of current node and its children
cost *= setMultiplier;
} else if (node.kind === Kind.FRAGMENT_SPREAD) {
if (this.visitedFragments.has(node.name.value)) {
const visitCost = this.visitedFragments.get(node.name.value) ?? 0;
Expand Down
82 changes: 80 additions & 2 deletions packages/plugins/cost-limit/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const typeDefinitions = `
}

type Query {
books: [Book]
books(first: Int, last: Int): [Book]
getBook(title: String): Book
}
`;
Expand All @@ -35,7 +35,15 @@ const books = [

const resolvers = {
Query: {
books: () => books,
books: (_: any, { first, last }: { first?: number; last?: number }) => {
if (first !== undefined) {
return books.slice(0, first);
}
if (last !== undefined) {
return books.slice(-last);
}
return books;
},
getBook: (_: any, { title }: { title: string }) => books.find((book) => book.title === title),
},
};
Expand Down Expand Up @@ -353,4 +361,74 @@ describe('costLimitPlugin', () => {
`Syntax Error: Query Cost limit of ${maxCost} exceeded, found 12.`,
]);
});

it('supports pagination using first or last', async () => {
const maxCost = 20;
const testkit = createTestkit(
[
costLimitPlugin({
maxCost: maxCost,
objectCost: 4,
scalarCost: 2,
depthCostFactor: 1.5,
ignoreIntrospection: true,
}),
],
schema,
);
const result = await testkit.execute(`
query {
firstBooks: books(first: 1) {
title
author
}

lastBooks: books(last: 1) {
title
author
}
}
`);

assertSingleExecutionValue(result);
expect(result.errors).toBeUndefined();
});

it('rejects pagination using first or last when cost limit is exceeded', async () => {
const maxCost = 57;
const testkit = createTestkit(
[
costLimitPlugin({
maxCost: maxCost,
objectCost: 4,
scalarCost: 2,
depthCostFactor: 1.5,
ignoreIntrospection: true,
}),
],
schema,
);
const result = await testkit.execute(`
query {
firstBooks: books(first: 2) {
title
author
}
...BookFragmentLast3

fragment BookFragmentLast3 on Query {
books(last: 3) {
title
author
}
}
}
`);

assertSingleExecutionValue(result);
expect(result.errors).toBeDefined();
expect(result.errors?.map((error) => error.message)).toEqual([
`Syntax Error: Query Cost limit of ${maxCost} exceeded, found 77.`,
]);
});
});
Loading