diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7554de9..dad43a5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,8 +13,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20, 22, 24, 26] - graphql-version: ['~15.0', '~16.0'] + node-version: [22, 24, 26] + graphql-version: ['~16.6', '~16', '~17.0'] steps: - name: Checkout repository uses: actions/checkout@v2 @@ -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 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': @@ -303,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; @@ -314,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; @@ -329,7 +326,7 @@ export default class QueryComplexity { } switch (childNode.kind) { - case Kind.FIELD: { + case 'Field': { let field = null; switch (childNode.name.value) { @@ -359,7 +356,7 @@ export default class QueryComplexity { args = getArgumentValues( field, childNode, - this.variableValues || {} + getExecutionVariableValues(this.variableValues) ); } catch (e) { this.context.reportError(e); @@ -414,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 @@ -462,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 @@ -499,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 ); @@ -534,6 +540,47 @@ export default class QueryComplexity { } } +/** + * GraphQL v17 changed getVariableValues() to return { variableValues } + * (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 getOperationVariableValues( + schema: GraphQLSchema, + variableDefinitions: readonly VariableDefinitionNode[], + inputs: Record +): { + variableValues: Record; + errors?: ReadonlyArray; +} { + const result = getVariableValues(schema, variableDefinitions, inputs) as { + coerced?: Record; + variableValues?: Record; + errors?: ReadonlyArray; + }; + + return { + 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 6f915ad..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": - 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"