From fd4f099266403a7558e5ffe85c7ee47ddfa005ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivo=20Mei=C3=9Fner?= Date: Sat, 18 Jul 2026 07:42:05 -0500 Subject: [PATCH 1/3] Add GraphQL v17 support --- .github/workflows/ci.yaml | 2 +- package.json | 4 ++-- src/QueryComplexity.ts | 28 +++++++++++++++++++++++++++- yarn.lock | 2 +- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7554de9..60825ba 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: strategy: matrix: node-version: [20, 22, 24, 26] - graphql-version: ['~15.0', '~16.0'] + graphql-version: ['~15.0', '~16.0', '~17.0'] steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/package.json b/package.json index 709b61e..3bfe672 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "lodash.get": "^4.4.2" }, "peerDependencies": { - "graphql": "^15.0.0 || ^16.0.0" + "graphql": "^15.0.0 || ^16.0.0 || ^17.0.0" }, "files": [ "dist", @@ -66,7 +66,7 @@ "@typescript-eslint/parser": "^5.1.0", "chai": "^4.3.4", "eslint": "^8.0.1", - "graphql": "~14.6.0 || ~15.0.0 || ~16.0.0", + "graphql": "~14.6.0 || ~15.0.0 || ~16.0.0 || ~17.0.0", "mocha": "^11.7.6", "prettier": "^2.4.1", "rimraf": "^3.0.2", diff --git a/src/QueryComplexity.ts b/src/QueryComplexity.ts index f5cc027..5ce9374 100644 --- a/src/QueryComplexity.ts +++ b/src/QueryComplexity.ts @@ -14,6 +14,7 @@ import { ValidationContext, FragmentDefinitionNode, OperationDefinitionNode, + VariableDefinitionNode, FieldNode, FragmentSpreadNode, InlineFragmentNode, @@ -187,7 +188,7 @@ export default class QueryComplexity { // Get variable values from variables that are passed from options, merged // with default values defined in the operation - const { coerced, errors } = getVariableValues( + const { coerced, errors } = getCoercedVariableValues( this.context.getSchema(), // We have to create a new array here because input argument is not readonly in graphql ~14.6.0 operation.variableDefinitions ? [...operation.variableDefinitions] : [], @@ -534,6 +535,31 @@ export default class QueryComplexity { } } +/** + * GraphQL v17 changed getVariableValues() to return { variableValues } + * instead of { coerced }. This helper supports both shapes. + */ +function getCoercedVariableValues( + schema: GraphQLSchema, + variableDefinitions: readonly VariableDefinitionNode[], + inputs: Record +): { coerced: Record; errors?: ReadonlyArray } { + const result = getVariableValues( + schema, + variableDefinitions ?? [], + inputs + ) as { + coerced?: Record; + variableValues?: { coerced: Record }; + errors?: ReadonlyArray; + }; + + return { + coerced: result.variableValues?.coerced ?? result.coerced ?? {}, + errors: result.errors, + }; +} + /** * Adds a complexity to the complexity map for all possible types * @param complexity diff --git a/yarn.lock b/yarn.lock index 6f915ad..1785ecb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -721,7 +721,7 @@ globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" -"graphql@~14.6.0 || ~15.0.0 || ~16.0.0": +"graphql@~14.6.0 || ~15.0.0 || ~16.0.0 || ~17.0.0": version "16.0.1" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.0.1.tgz#93a13cd4e0e38ca8d0832e79614c8578bfd34f10" integrity sha512-oPvCuu6dlLdiz8gZupJ47o1clgb72r1u8NDBcQYjcV6G/iEdmE11B1bBlkhXRvV0LisP/SXRFP7tT6AgaTjpzg== From 9b003158c6fe955c02c427e7bc7d7b064455cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivo=20Mei=C3=9Fner?= Date: Mon, 20 Jul 2026 21:27:44 -0500 Subject: [PATCH 2/3] Drop graphql v15 support --- .github/workflows/ci.yaml | 2 +- fix-hybrid-module.sh | 30 ++------- fix-hybrid-module.test.esm.sh | 15 +---- package.json | 4 +- src/QueryComplexity.ts | 67 ++++++++++++------- src/__tests__/QueryComplexity-test.ts | 4 +- .../fieldExtensionsEstimator-test.ts | 4 +- tsconfig.json | 2 +- yarn.lock | 8 +-- 9 files changed, 64 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 60825ba..97749ef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: strategy: matrix: node-version: [20, 22, 24, 26] - graphql-version: ['~15.0', '~16.0', '~17.0'] + graphql-version: ['~16.6', '~16', '~17.0'] steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/fix-hybrid-module.sh b/fix-hybrid-module.sh index 3c46b4e..ee5d7c0 100755 --- a/fix-hybrid-module.sh +++ b/fix-hybrid-module.sh @@ -7,20 +7,6 @@ cat >dist/cjs/package.json <dist/esm/package.json <dist/esm/package.json <dist/test/esm/package.json < this.context.reportError(error)); return; } - this.variableValues = coerced; + this.variableValues = variableValues; switch (operation.operation) { case 'query': @@ -304,7 +300,7 @@ export default class QueryComplexity { const values = getDirectiveValues( this.includeDirectiveDef, childNode, - this.variableValues || {} + getExecutionVariableValues(this.variableValues) ); if (typeof values.if === 'boolean') { includeNode = values.if; @@ -315,7 +311,7 @@ export default class QueryComplexity { const values = getDirectiveValues( this.skipDirectiveDef, childNode, - this.variableValues || {} + getExecutionVariableValues(this.variableValues) ); if (typeof values.if === 'boolean') { skipNode = values.if; @@ -330,7 +326,7 @@ export default class QueryComplexity { } switch (childNode.kind) { - case Kind.FIELD: { + case 'Field': { let field = null; switch (childNode.name.value) { @@ -360,7 +356,7 @@ export default class QueryComplexity { args = getArgumentValues( field, childNode, - this.variableValues || {} + getExecutionVariableValues(this.variableValues) ); } catch (e) { this.context.reportError(e); @@ -415,7 +411,7 @@ export default class QueryComplexity { } break; } - case Kind.FRAGMENT_SPREAD: { + case 'FragmentSpread': { const fragmentName = childNode.name.value; const fragment = this.context.getFragment(fragmentName); // Unknown fragment, should be caught by other validation rules @@ -463,7 +459,7 @@ export default class QueryComplexity { } break; } - case Kind.INLINE_FRAGMENT: { + case 'InlineFragment': { let inlineFragmentType: GraphQLNamedType = typeDef; if (childNode.typeCondition && childNode.typeCondition.name) { inlineFragmentType = this.context @@ -500,8 +496,17 @@ export default class QueryComplexity { break; } default: { + // Unreachable: all selection kinds (Field, FragmentSpread, + // InlineFragment) are handled above. The cast keeps this + // compatible across graphql versions whose AST `kind` typings + // differ (enum vs string literal), which affect how the switch + // narrows the node type in this branch. innerComplexities = addComplexities( - this.nodeComplexity(childNode, typeDef, activeFragments), + this.nodeComplexity( + childNode as FieldNode, + typeDef, + activeFragments + ), complexities, possibleTypeNames ); @@ -537,29 +542,45 @@ export default class QueryComplexity { /** * GraphQL v17 changed getVariableValues() to return { variableValues } - * instead of { coerced }. This helper supports both shapes. + * (an object with a `coerced` map) instead of a `{ coerced }` map directly. + * This helper normalizes both shapes to the container the running graphql + * version expects, without referencing any version-specific graphql types + * (which would leak into this package's published type definitions). */ -function getCoercedVariableValues( +function getOperationVariableValues( schema: GraphQLSchema, variableDefinitions: readonly VariableDefinitionNode[], inputs: Record -): { coerced: Record; errors?: ReadonlyArray } { - const result = getVariableValues( - schema, - variableDefinitions ?? [], - inputs - ) as { +): { + variableValues: Record; + errors?: ReadonlyArray; +} { + const result = getVariableValues(schema, variableDefinitions, inputs) as { coerced?: Record; - variableValues?: { coerced: Record }; + variableValues?: Record; errors?: ReadonlyArray; }; return { - coerced: result.variableValues?.coerced ?? result.coerced ?? {}, + variableValues: result.variableValues ?? result.coerced ?? {}, errors: result.errors, }; } +/** + * Returns the variable values in the form expected by getArgumentValues / + * getDirectiveValues. graphql >= 17 receives the `{ coerced, sources }` + * container as-is; graphql <= 16 receives the plain coerced map. An empty map + * (no variables) is passed as undefined for both. + */ +function getExecutionVariableValues(variableValues: Record): any { + if (!variableValues || Object.keys(variableValues).length === 0) { + return undefined; + } + + return variableValues; +} + /** * Adds a complexity to the complexity map for all possible types * @param complexity diff --git a/src/__tests__/QueryComplexity-test.ts b/src/__tests__/QueryComplexity-test.ts index 6815f7c..304dc59 100644 --- a/src/__tests__/QueryComplexity-test.ts +++ b/src/__tests__/QueryComplexity-test.ts @@ -494,8 +494,8 @@ describe('QueryComplexity analysis', () => { }); visit(ast, visitWithTypeInfo(typeInfo, visitor)); expect(context.getErrors().length).to.equal(1); - expect(context.getErrors()[0].message).to.equal( - 'Argument "count" of required type "Int!" was not provided.' + expect(context.getErrors()[0].message).to.match( + /^Argument "(?:count|Query\.requiredArgs\(count:\))" of required type "Int!" was not provided\.$/ ); }); diff --git a/src/estimators/fieldExtensions/__tests__/fieldExtensionsEstimator-test.ts b/src/estimators/fieldExtensions/__tests__/fieldExtensionsEstimator-test.ts index fe5f59a..424da1c 100644 --- a/src/estimators/fieldExtensions/__tests__/fieldExtensionsEstimator-test.ts +++ b/src/estimators/fieldExtensions/__tests__/fieldExtensionsEstimator-test.ts @@ -303,8 +303,8 @@ describe('fieldExtensions estimator', () => { }); visit(ast, visitWithTypeInfo(typeInfo, visitor)); expect(context.getErrors().length).to.equal(1); - expect(context.getErrors()[0].message).to.equal( - 'Argument "count" of required type "Int!" was not provided.' + expect(context.getErrors()[0].message).to.match( + /^Argument "(?:count|Query\.requiredArgs\(count:\))" of required type "Int!" was not provided\.$/ ); }); }); diff --git a/tsconfig.json b/tsconfig.json index b000795..3da7e62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "paths": { "*": ["node_modules/*", "src/types/*"] }, - "lib": ["esnext", "esnext.asynciterable"] + "lib": ["esnext", "esnext.asynciterable", "DOM"] }, "include": ["src/**/*"] } diff --git a/yarn.lock b/yarn.lock index 1785ecb..5c1dc60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -721,10 +721,10 @@ globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" -"graphql@~14.6.0 || ~15.0.0 || ~16.0.0 || ~17.0.0": - version "16.0.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.0.1.tgz#93a13cd4e0e38ca8d0832e79614c8578bfd34f10" - integrity sha512-oPvCuu6dlLdiz8gZupJ47o1clgb72r1u8NDBcQYjcV6G/iEdmE11B1bBlkhXRvV0LisP/SXRFP7tT6AgaTjpzg== +"graphql@~16.6.0 || ~17.0.0": + version "17.0.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-17.0.2.tgz#05ff6f18e0801e8d040d46957eba712c1459d5d7" + integrity sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ== has-flag@^4.0.0: version "4.0.0" From d79d32de2d87921c7ca6a7808b45114025664cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivo=20Mei=C3=9Fner?= Date: Mon, 20 Jul 2026 21:29:34 -0500 Subject: [PATCH 3/3] Drop node v20 support --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 97749ef..dad43a5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20, 22, 24, 26] + node-version: [22, 24, 26] graphql-version: ['~16.6', '~16', '~17.0'] steps: - name: Checkout repository @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20, 22, 24, 26] + node-version: [22, 24, 26] steps: - name: Checkout repository uses: actions/checkout@v2