Skip to content

Commit

Permalink
fix: inline fragments visitor (#375)
Browse files Browse the repository at this point in the history
* fix: inline fragments
  • Loading branch information
nullswan authored Mar 28, 2023
1 parent c321159 commit 8d0ab85
Show file tree
Hide file tree
Showing 9 changed files with 54 additions and 29 deletions.
9 changes: 9 additions & 0 deletions .changeset/six-readers-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@escape.tech/graphql-armor-max-directives': patch
'@escape.tech/graphql-armor-max-aliases': patch
'@escape.tech/graphql-armor-cost-limit': patch
'@escape.tech/graphql-armor-max-depth': patch
'@escape.tech/graphql-armor': patch
---

Fix: Inline fragment visitor (Thanks @simoncrypta @dthyresson)
4 changes: 1 addition & 3 deletions examples/apollo-v3/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ describe('startup', () => {
}`,
});
expect(query.errors).toBeDefined();
expect(query.errors?.map((e) => e.message)).toContain(
'Syntax Error: Query Cost limit of 100 exceeded, found 5023.',
);
expect(query.errors?.map((e) => e.message)).toContain('Syntax Error: Query Cost limit of 100 exceeded, found 138.');
});

it('should block field suggestion', async () => {
Expand Down
4 changes: 1 addition & 3 deletions examples/apollo/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ describe('startup', () => {

const query = body.singleResult;
expect(query.errors).toBeDefined();
expect(query.errors?.map((e) => e.message)).toContain(
'Syntax Error: Query Cost limit of 100 exceeded, found 5023.',
);
expect(query.errors?.map((e) => e.message)).toContain('Syntax Error: Query Cost limit of 100 exceeded, found 138.');
});

it('should block field suggestion', async () => {
Expand Down
2 changes: 1 addition & 1 deletion examples/yoga/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('startup', () => {
const body = JSON.parse(response.text);
expect(body.data?.books).toBeUndefined();
expect(body.errors).toBeDefined();
expect(body.errors?.map((e) => e.message)).toContain('Syntax Error: Query Cost limit of 100 exceeded, found 5023.');
expect(body.errors?.map((e) => e.message)).toContain('Syntax Error: Query Cost limit of 100 exceeded, found 138.');
});

it('should disable field suggestion', async () => {
Expand Down
18 changes: 11 additions & 7 deletions packages/plugins/cost-limit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class CostLimitVisitor {

private readonly context: ValidationContext;
private readonly config: Required<CostLimitOptions>;
private readonly visitedFragments: Set<string> = new Set();
private readonly visitedFragments: Map<string, number>;

constructor(context: ValidationContext, options?: CostLimitOptions) {
this.context = context;
Expand All @@ -45,6 +45,7 @@ class CostLimitVisitor {
costLimitDefaultOptions,
...Object.entries(options ?? {}).map(([k, v]) => (v === undefined ? {} : { [k]: v })),
);
this.visitedFragments = new Map();

this.OperationDefinition = {
enter: this.onOperationDefinitionEnter,
Expand Down Expand Up @@ -84,9 +85,6 @@ class CostLimitVisitor {
return node.selectionSet.selections.reduce((v, child) => v + this.computeComplexity(child, depth + 1), 0);
}

// const typeDefs: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType = this.context
// .getSchema()
// .getQueryType();
let cost = this.config.scalarCost;
if ('selectionSet' in node && node.selectionSet) {
cost = this.config.objectCost;
Expand All @@ -97,12 +95,18 @@ class CostLimitVisitor {

if (node.kind == Kind.FRAGMENT_SPREAD) {
if (this.visitedFragments.has(node.name.value)) {
return this.config.fragmentRecursionCost;
const visitCost = this.visitedFragments.get(node.name.value) ?? 0;
return cost + this.config.depthCostFactor * visitCost;
} else {
this.visitedFragments.set(node.name.value, -1);
}
this.visitedFragments.add(node.name.value);
const fragment = this.context.getFragment(node.name.value);
if (fragment) {
cost += this.config.depthCostFactor * this.computeComplexity(fragment, depth + 1);
const additionalCost = this.computeComplexity(fragment, depth + 1);
if (this.visitedFragments.get(node.name.value) === -1) {
this.visitedFragments.set(node.name.value, additionalCost);
}
cost += this.config.depthCostFactor * additionalCost;
}
}

Expand Down
4 changes: 1 addition & 3 deletions packages/plugins/cost-limit/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,6 @@ describe('global', () => {
`);
assertSingleExecutionValue(result);
expect(result.errors).toBeDefined();
expect(result.errors?.map((error) => error.message)).toContain(
'Syntax Error: Query Cost limit of 50 exceeded, found 16050.',
);
expect(result.errors?.map((error) => error.message)).toContain('Cannot spread fragment "A" within itself via "B".');
});
});
14 changes: 10 additions & 4 deletions packages/plugins/max-aliases/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class MaxAliasesVisitor {

private readonly context: ValidationContext;
private readonly config: Required<MaxAliasesOptions>;
private readonly visitedFragments: Set<string> = new Set();
private readonly visitedFragments: Map<string, number>;

constructor(context: ValidationContext, options?: MaxAliasesOptions) {
this.context = context;
Expand All @@ -33,6 +33,7 @@ class MaxAliasesVisitor {
maxAliasesDefaultOptions,
...Object.entries(options ?? {}).map(([k, v]) => (v === undefined ? {} : { [k]: v })),
);
this.visitedFragments = new Map();

this.OperationDefinition = {
enter: this.onOperationDefinitionEnter,
Expand Down Expand Up @@ -71,12 +72,17 @@ class MaxAliasesVisitor {
}
} else if (node.kind === Kind.FRAGMENT_SPREAD) {
if (this.visitedFragments.has(node.name.value)) {
return 0;
return this.visitedFragments.get(node.name.value) ?? 0;
} else {
this.visitedFragments.set(node.name.value, -1);
}
this.visitedFragments.add(node.name.value);
const fragment = this.context.getFragment(node.name.value);
if (fragment) {
aliases += this.countAliases(fragment);
const additionalAliases = this.countAliases(fragment);
if (this.visitedFragments.get(node.name.value) === -1) {
this.visitedFragments.set(node.name.value, additionalAliases);
}
aliases += additionalAliases;
}
}
return aliases;
Expand Down
14 changes: 10 additions & 4 deletions packages/plugins/max-depth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class MaxDepthVisitor {

private readonly context: ValidationContext;
private readonly config: Required<MaxDepthOptions>;
private readonly visitedFragments: Set<string> = new Set();
private readonly visitedFragments: Map<string, number>;

constructor(context: ValidationContext, options?: MaxDepthOptions) {
this.context = context;
Expand All @@ -34,6 +34,7 @@ class MaxDepthVisitor {
maxDepthDefaultOptions,
...Object.entries(options ?? {}).map(([k, v]) => (v === undefined ? {} : { [k]: v })),
);
this.visitedFragments = new Map();

this.OperationDefinition = {
enter: this.onOperationDefinitionEnter,
Expand Down Expand Up @@ -74,12 +75,17 @@ class MaxDepthVisitor {
}
} else if (node.kind == Kind.FRAGMENT_SPREAD) {
if (this.visitedFragments.has(node.name.value)) {
return 0;
return this.visitedFragments.get(node.name.value) ?? 0;
} else {
this.visitedFragments.set(node.name.value, -1);
}
this.visitedFragments.add(node.name.value);
const fragment = this.context.getFragment(node.name.value);
if (fragment) {
depth = Math.max(depth, this.countDepth(fragment, parentDepth + 1));
const fragmentDepth = this.countDepth(fragment, parentDepth + 1);
depth = Math.max(depth, fragmentDepth);
if (this.visitedFragments.get(node.name.value) === -1) {
this.visitedFragments.set(node.name.value, fragmentDepth);
}
}
}
return depth;
Expand Down
14 changes: 10 additions & 4 deletions packages/plugins/max-directives/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class MaxDirectivesVisitor {

private readonly context: ValidationContext;
private readonly config: Required<MaxDirectivesOptions>;
private readonly visitedFragments: Set<string> = new Set();
private readonly visitedFragments: Map<string, number>;

constructor(context: ValidationContext, options?: MaxDirectivesOptions) {
this.context = context;
Expand All @@ -35,6 +35,7 @@ class MaxDirectivesVisitor {
maxDirectivesDefaultOptions,
...Object.entries(options ?? {}).map(([k, v]) => (v === undefined ? {} : { [k]: v })),
);
this.visitedFragments = new Map();

this.OperationDefinition = {
enter: this.onOperationDefinitionEnter,
Expand Down Expand Up @@ -73,12 +74,17 @@ class MaxDirectivesVisitor {
}
} else if (node.kind == Kind.FRAGMENT_SPREAD) {
if (this.visitedFragments.has(node.name.value)) {
return 0;
return this.visitedFragments.get(node.name.value) ?? 0;
} else {
this.visitedFragments.set(node.name.value, -1);
}
this.visitedFragments.add(node.name.value);
const fragment = this.context.getFragment(node.name.value);
if (fragment) {
directives += this.countDirectives(fragment);
const additionalDirectives = this.countDirectives(fragment);
if (this.visitedFragments.get(node.name.value) === -1) {
this.visitedFragments.set(node.name.value, additionalDirectives);
}
directives += additionalDirectives;
}
}
return directives;
Expand Down

0 comments on commit 8d0ab85

Please sign in to comment.