Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 6 additions & 24 deletions fix-hybrid-module.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,16 @@ cat >dist/cjs/package.json <<!EOF
}
!EOF

# Define the file paths
cjs_file_path="dist/cjs/QueryComplexity.js"
esm_file_path="dist/esm/QueryComplexity.js"
find_path="dist/esm"

# Detect the operating system and use the appropriate sed command
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS (BSD sed)
sed -i '' 's/require("graphql\/execution\/values")/require("graphql\/execution\/values.js")/' "$cjs_file_path"
else
# Linux (GNU sed)
sed -i 's/require("graphql\/execution\/values")/require("graphql\/execution\/values.js")/' "$cjs_file_path"
fi

# Create package.json for ES modules
cat >dist/esm/package.json <<!EOF
{
"type": "module"
}
!EOF

# Detect the operating system and use the appropriate sed command
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS (BSD sed)
sed -i '' 's/from '\''graphql\/execution\/values'\'';/from '\''graphql\/execution\/values.mjs'\'';/' "$esm_file_path"
find "$find_path" -type f -name "*.js" -exec sed -i '' 's/from '\''graphql'\'';/from '\''graphql\/index.mjs'\'';/' {} +
else
# Linux (GNU sed)
sed -i 's/from '\''graphql\/execution\/values'\'';/from '\''graphql\/execution\/values.mjs'\'';/' "$esm_file_path"
find "$find_path" -type f -name "*.js" -exec sed -i 's/from '\''graphql'\'';/from '\''graphql\/index.mjs'\'';/' {} +
fi
# No import specifiers need rewriting: every graphql import resolves through
# the bare "graphql" package root. Using a single specifier for both builds
# guarantees a single graphql instance per realm in every environment
# (bundler, native ESM, CommonJS) across all supported graphql versions.
# The deep "graphql/execution/values" import is no longer used because
# getVariableValues/getArgumentValues are root exports since graphql 16.6.
15 changes: 2 additions & 13 deletions fix-hybrid-module.test.esm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,5 @@ cat >dist/test/esm/package.json <<!EOF
}
!EOF

file_path="dist/test/esm/QueryComplexity.js"
find_path="dist/test/esm"

# Detect the operating system and use the appropriate sed command
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS (BSD sed)
sed -i '' 's/from '\''graphql\/execution\/values'\'';/from '\''graphql\/execution\/values.mjs'\'';/' "$file_path"
find "$find_path" -type f -name "*.js" -exec sed -i '' 's/from '\''graphql'\'';/from '\''graphql\/index.mjs'\'';/' {} +
else
# Linux (GNU sed)
sed -i 's/from '\''graphql\/execution\/values'\'';/from '\''graphql\/execution\/values.mjs'\'';/' "$file_path"
find "$find_path" -type f -name "*.js" -exec sed -i 's/from '\''graphql'\'';/from '\''graphql\/index.mjs'\'';/' {} +
fi
# No import specifiers need rewriting: every graphql import resolves through
# the bare "graphql" package root (see fix-hybrid-module.sh for details).
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"lodash.get": "^4.4.2"
},
"peerDependencies": {
"graphql": "^15.0.0 || ^16.0.0"
"graphql": "^16.6.0 || ^17.0.0"
},
"files": [
"dist",
Expand Down Expand Up @@ -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": "~16.6.0 || ~17.0.0",
"mocha": "^11.7.6",
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
Expand Down
73 changes: 60 additions & 13 deletions src/QueryComplexity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import {
getArgumentValues,
getDirectiveValues,
getVariableValues,
} from 'graphql/execution/values';

import {
ValidationContext,
FragmentDefinitionNode,
OperationDefinitionNode,
VariableDefinitionNode,
FieldNode,
FragmentSpreadNode,
InlineFragmentNode,
Expand All @@ -32,7 +30,6 @@ import {
GraphQLUnionType,
GraphQLObjectType,
GraphQLInterfaceType,
Kind,
getNamedType,
GraphQLError,
SchemaMetaFieldDef,
Expand Down Expand Up @@ -187,7 +184,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 { variableValues, errors } = getOperationVariableValues(
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] : [],
Expand All @@ -198,7 +195,7 @@ export default class QueryComplexity {
errors.forEach((error) => this.context.reportError(error));
return;
}
this.variableValues = coerced;
this.variableValues = variableValues;

switch (operation.operation) {
case 'query':
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -329,7 +326,7 @@ export default class QueryComplexity {
}

switch (childNode.kind) {
case Kind.FIELD: {
case 'Field': {
let field = null;

switch (childNode.name.value) {
Expand Down Expand Up @@ -359,7 +356,7 @@ export default class QueryComplexity {
args = getArgumentValues(
field,
childNode,
this.variableValues || {}
getExecutionVariableValues(this.variableValues)
);
} catch (e) {
this.context.reportError(e);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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<string, any>
): {
variableValues: Record<string, any>;
errors?: ReadonlyArray<GraphQLError>;
} {
const result = getVariableValues(schema, variableDefinitions, inputs) as {
coerced?: Record<string, any>;
variableValues?: Record<string, any>;
errors?: ReadonlyArray<GraphQLError>;
};

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<string, any>): 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
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/QueryComplexity-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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\.$/
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\.$/
);
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"paths": {
"*": ["node_modules/*", "src/types/*"]
},
"lib": ["esnext", "esnext.asynciterable"]
"lib": ["esnext", "esnext.asynciterable", "DOM"]
},
"include": ["src/**/*"]
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading