Skip to content

Commit

Permalink
Feat/flatten fragments cost plugin (#724)
Browse files Browse the repository at this point in the history
* Update cost-limit.md

Signed-off-by: ideeeveee <[email protected]>

* add flattenFragments flag to cost-limit plugin

Introduce config to control if depthCostFactor should be applied to fragments when calculating the cost

Signed-off-by: ideeeveee <[email protected]>

* Add tests

Signed-off-by: ideeeveee <[email protected]>

* feat: changeset

---------

Signed-off-by: ideeeveee <[email protected]>
Co-authored-by: nullswan <[email protected]>
  • Loading branch information
ideeeveee and nullswan authored Oct 25, 2024
1 parent 9c39455 commit bc9e2d0
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/bright-kings-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@escape.tech/graphql-armor-cost-limit': minor
'@escape.tech/graphql-armor': patch
---

cost limit: flatten fragment option support
28 changes: 20 additions & 8 deletions packages/plugins/cost-limit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type CostLimitOptions = {
objectCost?: number;
scalarCost?: number;
depthCostFactor?: number;
flattenFragments?: boolean;
ignoreIntrospection?: boolean;
fragmentRecursionCost?: number;
exposeLimits?: boolean;
Expand All @@ -27,6 +28,7 @@ const costLimitDefaultOptions: Required<CostLimitOptions> = {
objectCost: 2,
scalarCost: 1,
depthCostFactor: 1.5,
flattenFragments: false,
fragmentRecursionCost: 1000,
ignoreIntrospection: true,
exposeLimits: true,
Expand Down Expand Up @@ -95,11 +97,16 @@ class CostLimitVisitor {
if ('selectionSet' in node && node.selectionSet) {
cost = this.config.objectCost;
for (const child of node.selectionSet.selections) {
cost += this.config.depthCostFactor * this.computeComplexity(child, depth + 1);
if (
this.config.flattenFragments &&
(child.kind === Kind.INLINE_FRAGMENT || child.kind === Kind.FRAGMENT_SPREAD)
) {
cost += this.computeComplexity(child, depth);
} else {
cost += this.config.depthCostFactor * this.computeComplexity(child, depth + 1);
}
}
}

if (node.kind === Kind.FRAGMENT_SPREAD) {
} else if (node.kind === Kind.FRAGMENT_SPREAD) {
if (this.visitedFragments.has(node.name.value)) {
const visitCost = this.visitedFragments.get(node.name.value) ?? 0;
return cost + this.config.depthCostFactor * visitCost;
Expand All @@ -108,14 +115,19 @@ class CostLimitVisitor {
}
const fragment = this.context.getFragment(node.name.value);
if (fragment) {
const additionalCost = this.computeComplexity(fragment, depth + 1);
let fragmentCost;
if (this.config.flattenFragments) {
fragmentCost = this.computeComplexity(fragment, depth);
cost += fragmentCost;
} else {
fragmentCost = this.computeComplexity(fragment, depth + 1);
cost += this.config.depthCostFactor * fragmentCost;
}
if (this.visitedFragments.get(node.name.value) === -1) {
this.visitedFragments.set(node.name.value, additionalCost);
this.visitedFragments.set(node.name.value, fragmentCost);
}
cost += this.config.depthCostFactor * additionalCost;
}
}

return cost;
}
}
Expand Down
67 changes: 67 additions & 0 deletions packages/plugins/cost-limit/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,73 @@ describe('costLimitPlugin', () => {
]);
});

it('should flatten fragments', async () => {
const maxCost = 57;
const testkit = createTestkit(
[
costLimitPlugin({
maxCost: maxCost,
objectCost: 4,
scalarCost: 2,
depthCostFactor: 2,
flattenFragments: true,
ignoreIntrospection: true,
}),
],
schema,
);
const result = await testkit.execute(`
query {
...BookFragment
}
fragment BookFragment on Query {
books {
title
author
}
}
`);

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

it('should reject flattened fragments if the maxCost limit is exceeded', async () => {
const maxCost = 5;
const testkit = createTestkit(
[
costLimitPlugin({
maxCost: maxCost,
objectCost: 4,
scalarCost: 2,
depthCostFactor: 2,
flattenFragments: true,
ignoreIntrospection: true,
}),
],
schema,
);
const result = await testkit.execute(`
query {
...BookFragment
}
fragment BookFragment on Query {
books {
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 30.`,
]);
});

it('should not crash on recursive fragment', async () => {
const testkit = createTestkit(
[
Expand Down
3 changes: 3 additions & 0 deletions services/docs/docs/plugins/cost-limit.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ GraphQLArmorConfig({
// Factorial applied to nested operator | default: 1.5
depthCostFactor?: int,

// Flatten frament spreads and inline framents for the cost calculation | default: false
flattenFragments?: boolean,

// Ignore the cost of introspection queries | default: true
ignoreIntrospection?: boolean,

Expand Down

0 comments on commit bc9e2d0

Please sign in to comment.